参考
http://cantellow.iteye.com/blog/838473
;公众号:程序员小灰
- 懒汉式
- 线程不安全:
- 这种写法lazy loading很明显,但是致命的是在多线程不能正常工作。需要的时候才会实例化
- 对比上一中,就多个Synchronized关键字,这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。
- 这种方式基于classloder机制,在深度分析Java的ClassLoader机制(源码级别)和Java类的加载、链接和初始化两个文章中有关于CLassload而机制的线程安全问题的介绍,避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。
- 饿汉与懒汉:饿汉就是自己找东西,自己实例化好了,懒汉就是需要别人来喂,别人来实例化
- 其实这是种仍然不是绝对的线程安全
- 假设这样的场景,当两个线程一先一后访问getInstance方法的时候,当A线程正在构建对象,B线程刚刚进入方法:
- 这种情况表面看似没什么问题,要么Instance还没被线程A构建,线程B执行 if(instance == null)的时候得到true;要么Instance已经被线程A构建完成,线程B执行 if(instance == null)的时候得到false。真的如此吗?答案是否定的。这里涉及到了JVM编译器的指令重排。指令重排是什么意思呢?比如java中简单的一句 instance = new Singleton,会被编译器编译成如下JVM指令:
- 但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:
- 当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。如下图所示:
- 改良后:
- 两者区别,也就是多了个Volatile关键字
- 经过volatile的修饰,当线程A执行instance = new Singleton的时候,JVM执行顺序是什么样?始终保证是下面的顺序:
- 如此在线程B看来,instance对象的引用要么指向null,要么指向一个初始化完毕的Instance,而不会出现某个中间态,保证了安全。
- .从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象INSTANCE。
- .INSTANCE对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类LazyHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。
- 代码可以简单归纳为三个步骤:
- 第一步,获得单例类的构造器。
- 第二步,把构造器设置为可访问。
- 第三步,使用newInstance方法构造对象。
- 最后为了确认这两个对象是否真的是不同的对象,我们使用equals方法进行比较。毫无疑问,比较结果是false。
- 为了检测是否能通过反射来打破通过枚举产生的单例:
- 然后出现
- 总结:
- volatile关键字不但可以防止指令重排,也可以保证线程访问的变量值是主内存中的最新值。有关volatile的详细原理,我在以后的漫画中会专门讲解。
- .使用枚举实现的单例模式,不但可以防止利用反射强行构建单例对象,而且可以在枚举类对象被反序列化的时候,保证反序列的返回结果是同一对象。
- 对于其他方式实现的单例模式,如果既想要做到可序列化,又想要反序列化为同一对象,则必须实现readResolve方法。
github:https://github.com/wk1995/DesignPattern