嘎里三分熟
  • 首页
  • JMusic
  • TSBay
  • 常用工具
  • About Me
  • 留言板
一行代码一世浮生
  1. 首页
  2. Java基础
  3. 正文

使用枚举来写出更优雅的单例设计模式

2017年12月08日 3733点热度 7人点赞 0条评论

一、最常见的单例

    我们先展示一段最常见的懒汉式的单例:

public class Singleton {
    private Singleton(){} // 私有构造
    private static Singleton instance = null; // 私有单例对象
    // 静态工厂
    public static Singleton getInstance(){
        if (instance == null) { // 双重检测机制
            synchronized (Singleton.class) { // 同步锁
                if (instance == null) { // 双重检测机制
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

    上述单例的写法采用的双重检查机制增加了一定的安全性,但是没有考虑到 JVM 编译器的指令重排。

二、杜绝 JVM 的指令重排对单例造成的影响

    1、什么是指令重排

        比如 java 中简单的一句 instance = new Singleton,会被编译器编译成如下 JVM 指令:

  memory =allocate();    //1:分配对象的内存空间 
  ctorInstance(memory);  //2:初始化对象 
  instance =memory;     //3:设置instance指向刚分配的内存地址

        但是这些指令顺序并非一成不变,有可能会经过 JVM 和 CPU 的优化,指令重排成下面的顺序:

    memory =allocate();    //1:分配对象的内存空间 
    instance =memory;     //3:设置instance指向刚分配的内存地址 
    ctorInstance(memory);  //2:初始化对象

    2、影响

        对应到上文的单例模式,会产生如下图的问题:

            ① 当线程 A 执行完1,3,时,准备走2,即 instance 对象还未完成初始化,但已经不再指向 null 。

            ② 此时如果线程 B 抢占到CPU资源,执行  if(instance == null)的结果会是 false,

            ③ 从而返回一个没有初始化完成的instance对象。

            singleton01.png

    3、解决

        如何去防止呢,很简单,可以利用关键字 volatile 来修饰 instance 对象,如下图进行优化:

            singleton02.png

        why?

            很简单,volatile 修饰符在此处的作用就是阻止变量访问前后的指令重排,从而保证了指令的执行顺序。

            意思就是,指令的执行顺序是严格按照上文的 1、2、3 来执行的,从而对象不会出现中间态。

        其实,volatile 关键字在多线程的开发中应用很广,暂不赘述。

        虽然很赞,但是此处仍然没有考虑过反射机制带来的影响。

三、进阶篇,实现完美单例

    1、小插曲

        实现单例有很多种模式,在此介绍一种使用静态内部类实现单例模式的方式:

public class Singleton {
    private static class LazyHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton (){}
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

        这是一种很巧妙的方式,原由是:

            1)从外部无法访问静态内部类 LazyHolder,只有当调用 Singleton.getInstance() 方法的时候,才能得到单例对象 INSTANCE。

            2)INSTANCE 对象初始化的时机并不是在单例类 Singleton 被加载的时候,而是在调用 getInstance 方法,使得静态内部类 LazyHolder 被加载的时候。

            3)因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。

    2、漏洞展示

        很多种单例的写法都有一个通病,就是无法防止反射机制的漏洞,从而无法保证对象的唯一性,如下举例:

            利用如下的反正代码对上文构造的单例进行对象的创建。

public static void main(String[] args) {
    try {
        //获得构造器
        Constructor con = Singleton.class.getDeclaredConstructor();
        //设置为可访问
        con.setAccessible(true);
        //构造两个不同的对象
        Singleton singleton1 = (Singleton)con.newInstance();
        Singleton singleton2 = (Singleton)con.newInstance();
        //验证是否是不同对象
        System.out.println(singleton1);
        System.out.println(singleton2);
        System.out.println(singleton1.equals(singleton2));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

            我们直接看结果:

                singleton03.png

            结果很明显,这显然是两个对象。

    3、解决

        使用枚举来实现单例模式。

        实现很简单,就三行代码:

public enum Singleton {
    INSTANCE;
}

        上面所展示的就是一个单例,

        why?

            其实这就是 enum 的一块语法糖,JVM 会阻止反射获取枚举类的私有构造方法。

        仍然使用上文的反射代码来进行测试,发现,报错。嘿嘿,完美解决反射的问题。

    4、缺点

        使用枚举的方法是起到了单例的作用,但是也有一个弊端,

        那就是  无法进行懒加载。

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: java JVM volatile 单例 反射 指令重排 枚举 设计模式
最后更新:2017年12月08日

GoldenJet

爱折腾技术的90后漫威小死忠程序员一枚

点赞
< 上一篇
下一篇 >

文章评论

取消回复

通过电子邮件订阅博客

分类目录
  • BootStrap (2)
  • Bug集中营 (6)
  • Java web (3)
  • JavaScript (7)
  • Java基础 (17)
  • Java工具 (5)
  • Linux (3)
  • Python (3)
  • SpringBoot (14)
  • Spring基础 (8)
  • thymeleaf (1)
  • 娱乐 (3)
  • 小谈 (2)
  • 常用工具 (7)
  • 技术分析集 (5)
  • 技能 (10)
  • 源码 (4)
  • 科普类 (1)
  • 算法 (9)
  • 踩坑记 (5)
文章归档
  • 2020年11月 (1)
  • 2020年7月 (1)
  • 2020年4月 (2)
  • 2020年3月 (1)
  • 2020年1月 (1)
  • 2019年11月 (1)
  • 2019年10月 (1)
  • 2019年9月 (1)
  • 2019年8月 (1)
  • 2019年7月 (2)
  • 2019年5月 (2)
  • 2019年4月 (2)
  • 2019年3月 (3)
  • 2019年2月 (2)
  • 2019年1月 (2)
  • 2018年12月 (2)
  • 2018年11月 (3)
  • 2018年10月 (3)
  • 2018年9月 (2)
  • 2018年8月 (3)
  • 2018年7月 (2)
  • 2018年5月 (1)
  • 2018年4月 (3)
  • 2018年3月 (2)
  • 2018年2月 (3)
  • 2018年1月 (5)
  • 2017年12月 (2)
  • 2017年11月 (3)
  • 2017年10月 (1)
  • 2017年9月 (1)
  • 2017年8月 (1)
  • 2017年7月 (7)
  • 2017年6月 (5)
  • 2017年5月 (1)
  • 2017年4月 (2)
  • 2017年3月 (4)
  • 2017年2月 (2)
小伙伴友链
  • 前端驿站

COPYRIGHT © 2017-2023 嘎里三分熟. ALL RIGHTS RESERVED.

浙ICP备17005575号-1

浙公网安备 33010802009043号