原文: Gerrard_Feng
二:2-懒汉模式(Lazy)
思想:相比于饿汉模式,懒汉模式实际中的应用更多,因为在系统中,“被用到时再初始化”是更佳的解决方案。
设计思想与饿汉模式类似,同样是持有一个自身的引用,只是将 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 方法?
因为是延迟加载,考虑到多线程情况,需要对方法同步。 -
同步方法带来的性能问题?
可以使用 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() 方法的时候才第一次初始化单例对象。
但是这么做无法保证在多线程环境下只创建一个对象。
显然,假设有多个线程同时调用 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 的两个作用之一,另一个作用是保证共享变量的可见性,这里不深入展开)
优势?劣势?
优势:延迟加载。
劣势:不能完全屏蔽反射入侵,而且代码较为繁琐。