原文: Gerrard_Fenghtml
二:2-懒汉模式(Lazy)
思想:相比于饿汉模式,懒汉模式实际中的应用更多,由于在系统中,“被用到时再初始化”是更佳的解决方案。java
设计思想与饿汉模式相似,一样是持有一个自身的引用,只是将 new 的动做延迟到 getinstance() 方法中执行。多线程
public final class LazySingleton { private static LazySingleton instance; private LazySingleton() { if (instance != null) { throw new IllegalStateException(); } } public static synchronized LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; } }
反射可否打破单例?
对于 LazySingleton,这是个颇有趣的问题,虽然咱们在私有构造器中增长了 instance==null 的判断,可是因为延迟加载的缘由,使得它没法完美地规避反射的入侵。性能
这涉及到了反射入侵和 getInstance() 方法调用顺序的问题。若是在调用 getInstance() 方法以前进行反射入侵,那么就会打破单例,反之,能够保证单例。优化
public class LazySingletonTest { [@Test](https://my.oschina.net/azibug) public void testReflectSuccess() throws Exception { Constructor<?> constructor = LazySingleton1.class.getDeclaredConstructor(); constructor.setAccessible(true); LazySingleton1 singleton1 = (LazySingleton1) constructor.newInstance(); LazySingleton1 singleton2 = LazySingleton1.getInstance(); Assert.assertNotSame(singleton1, singleton2); } [@Test](https://my.oschina.net/azibug) public void testReflectFailure() throws Exception { LazySingleton1 singleton1 = LazySingleton1.getInstance(); Constructor<?> constructor = LazySingleton1.class.getDeclaredConstructor(); constructor.setAccessible(true); try { LazySingleton1 singleton2 = (LazySingleton1) constructor.newInstance(); Assert.fail(); } catch (Exception e) { // Do nothing, test pass } } }
为何是 synchronized 方法?
由于是延迟加载,考虑到多线程状况,须要对方法同步。.net
同步方法带来的性能问题?
可使用 synchronized 代码块 + Double-check Locking + volatile 关键字,对 LazySingleton 进行深一步优化:线程
Step1:基础的懒汉模式翻译
public class LazySingleton { private static LazySingleton instance = null; private LazySingleton() { } public static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; } }
基础的懒汉模式保证了在调用 getInstance() 方法的时候才第一次初始化单例对象。设计
可是这么作没法保证在多线程环境下只建立一个对象。code
显然,假设有多个线程同时调用 getInstance() 方法,在第一个线程执行完毕以前,会有多个 LazyInstance 对象被建立。
Step2:为 getInstance() 方法加上同步锁
public class LazySingleton { private static LazySingleton instance = null; private LazySingleton() { } public synchronized static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; } }
经过简单地在方法上加上同步锁,能够保证同时只有一个线程调用这个静态方法,从而保证在多线程环境下的单例。
然而这么作有明显的 性能 隐患。
假设有多个线程想要获取 instance,不管此时对象是否已经被建立,都要频繁地获取锁,释放锁。这种作法很影响效率。
Step3:在 getInstance() 方法内部增长同步代码块
public class LazySingleton { private static LazySingleton instance = null; private LazySingleton() { } public static LazySingleton getInstance() { if (instance == null) { synchronized (LazySingleton.class) { instance = new LazySingleton(); } } return instance; } }
既然在方法上加同步锁不合适,那么就在方法内部增长同步代码块。
在判断 instance == null 以后,增长的同步代码块就不会产生 performance 问题,由于以后的访问会直接 return,不会进入同步代码块。
可是这么作,不能完整地保证单例。
参照 Step1,假设有多线程调用,且都经过了 instance == null 的判断,那么同样会有多个 LazySingleton 对象被建立。
Step4:使用 Double-Checked Locking
public class LazySingleton { private static LazySingleton instance = null; private LazySingleton() { } public static LazySingleton getInstance() { if (instance == null) { synchronized (LazySingleton.class) { if (instance == null) { instance = new LazySingleton(); } } } return instance; } }
经过增长双重判断( Double-Checked Locking),以及同步代码块,就能够避免 Step3 中可能出现的隐患。
可是 Double-Checked Locking 虽然可以保证单例的建立,可是在多线程的状况下可能出现某个线程使用建立不彻底的对象的状况。
Step5:使用 volatile 关键字修饰字段 instance
public class LazySingleton { private static volatile LazySingleton instance = null; private LazySingleton() { } public static LazySingleton getInstance() { if (instance == null) { synchronized (LazySingleton.class) { if (instance == null) { instance = new LazySingleton(); } } } return instance; } }
参考文档:The "Double-Checked Locking is Broken" Declaration
若是不适应英文描述,ImportNew 对这篇文档进行了翻译:能够不要再使用Double-Checked Locking了
这里面讲述了 Double-Checked Locking 在懒汉模式下可能出现的问题。
主要问题在于 Java 指令重排。
当 Java 代码被编译器翻译成字节码被存储在 JVM 时,为了提升性能,编译器会对这些操做指令进行指令重排。
也就是说,代码在计算机上执行的顺序,会被打乱。
返回到本例的问题,懒汉模式最关键的2个操做:
1.在 heap 中建立一个 LazyInstance 对象。 2.为字段 instance 赋值。 假设操做1在操做2以前被执行,那么代码就没有问题。
反之若操做2在操做1以前被执行,若是不能保证建立 LazyInstance 对象的过程是原子的,那么代码仍是会出现问题,由于 instance 指向了一个没有被建立彻底的对象。
事实上,引用类型和64位类型(long 和 double)都不能被原子地读写。
解决方案是经过 volatile 关键字来禁止指令重排(这是 volatile 的两个做用之一,另外一个做用是保证共享变量的可见性,这里不深刻展开)
优点?劣势?
优点:延迟加载。
劣势:不能彻底屏蔽反射入侵,并且代码较为繁琐。