首先,这个问题是从《阿里巴巴Java开发手册》的1.6.12(P31)上面看到的,里面有这样一句话,并列出一种反例代码(以下为仿写,并非与书上一致):
在并发场景下,通过双重检查锁(double-checked locking)实现延迟初始化的优化问题隐患,推荐解决方案中较为简单的一种(适用于JDK5及以上的版本),即目标属性声明为volatile型。
1 public class Singleton { 2 private static Singleton instance=null; 3 private Singleton() { 4 } 5 6 public static Singleton getInstance() { 7 if (instance == null) {//1 8 synchronized (Singleton.class) {//2 9 if (instance == null) {//3 10 instance = new Singleton();//4 11 } 12 } 13 } 14 return instance; 15 } 16 }
这样的单例模式经常容易看到(我之前也是这样写的),这样的问题在于初始化代码:
instance = new Singleton();
JVM会将这段代码分成三步去执行:
a.分配内存空间;
b.构造Singleton;
c.将instance指向构造的实例。
如果执行的过程是a->b->c的话,那上面的代码是没有问题的,但是有时JVM会基于指令优化的目的将指令重排,导致指令执行流程变为a->c->b。这样当线程A执行到4开始初始化单例对象的c流程时,线程B执行到1处,由于instance对象已经将内部指针指向分配的内存空间(即不为null),会直接返回未完全构造好的实例,从而出错。按照《手册》的说法,修改后的代码如下
1 public class Singleton { 2 private static volatile Singleton instance=null; //添加volatile修饰符 3 private Singleton() { 4 } 5 6 public static Singleton getInstance() { 7 if (instance == null) {//1 8 synchronized (Singleton.class) {//2 9 if (instance == null) {//3 10 instance = new Singleton();//4 11 } 12 } 13 } 14 return instance; 15 } 16 }
由于volatile自带的“禁止指令重优化”语义,初始化语句只能按照a->b->c的顺序进行执行。详细的解释可以参考这篇文章:《Java单例模式中双重检查锁的问题》
注:尽管这个问题看起来很简单,但是我在本地没有办法重演这个bug,这个bug出现的关键时刻在于线程A在执行a->c->b链的c时,线程B将构造完的instance返回并使用才会出错,但是一般的场景下是没有办法在这么短的时间间隔内捕获到这个间隔的。不过出于保险的目的,单例模式的我还是加上volatile修饰符比较好。