简介
在Java程序中,单例模式是很常见的模式。通常需要延迟初始化来降低初始化程序时对象创建的开销,通常使用的方法是双重锁检查,但是不加volatile限制的双重锁检查是一个错误的用法,正确的做法是通过volatile或者基于类初始化的方法来解决线程安全问题。
一、基于 volatile 的解决方案
1 | public class SafeVolatileInstance { |
instance = SafeVolatileInstance();这个操作按照如下三个步骤进行- 由于所有线程在执行程序的时候必须要遵守 intra-thread semantics , intra-thread semantics 保证重排序不会改变单线程中的执行结果,所以步骤2和3可能会发生重排序,当步骤3先执行后,如果此时有另一个线程B访问getInstance方法,第一个if中instance == null 就会返回false,但此时instance对象并未真正初始化成功,线程B将访问到一个未初始化的对象,这就是双重锁检查的问题所在
- 当instance使用volatile修饰之后, 2和3之间的重排序在多线程的环境中会被禁止 ,所以不会出现上述的访问到未初始化的对象的问题。

需要基于JDK5以上的版本,从JDK5开始使用的新的JSR-133内存模型规范增强了volatile的语义。
二、基于类初始化的解决方案
1 | public class SafeHolderInstance { |
这个方案的实质是:允许上述的2和3重排序,但不允许非构造线程“看到”这个重排序
初始化一个类,包括执行这个类的静态初始化和初始化静态字段。根据Java语言规范,类或接口T在首次发生如下任意一种情况时,会被立即初始化:
- T 是一个类,而且一个T的实例被创建
- T 是一个类,且T中声明的静态方法被调用
- T 中声明的一个静态字段被赋值
- T 中声明的一个静态字段被使用,而且这个字段不是一个常量字段
- T 是一个顶级类(Top level class),而且一个断言语句嵌套在T内被执行
在如上代码中,首次执行getInstance方法时 InstanceHolder会被初始化
多个线程同时调用getInstance方法时,会导致多个线程同时尝试初始化 InstanceHolder 类,Java语言规范规定,对于每一个类或者接口C,都有一个唯一的初始化锁LC与之对应。JVM在初始化期间会获取这个初始化锁,并且每个线程至少获取一次锁来确保这个类已经被初始化过了。初始化锁的实现可以确保类初始化只会由一个线程完成,当初始化完成之后,其他线程看到的是一个初始化好的instance实例,保证不会出现访问到不正常的对象的情况。具体的类初始化的锁同步问题可以参考《Java 并发编程的艺术》。
总结
对比两种方法会发现基于类初始化的方案更加简洁。但是基于volatile的双重检查锁定的方案有一个额外的优势,除了可以对静态字段实现延迟初始化,也可以对实例字段实现延迟初始化。
所以,如果需要对实例字段使用线程安全的延迟初始化,使用基于volatile的延迟初始化方案;如果需要对静态字段使用线程安全的延迟初始化,使用基于类初始化的方案。