这个例子非常经典。
1 | public final class Singleton { |
double check lock 是一种提升多线程之间效率的表现。
如果只是
1 | public final class Singleton { |
那么,每次进来都需要进入 synchronized ,非常消耗资源。所以,在外层加了 INSTANCE 判断。
这样看似更合理,并且更快,但是,这样反而是线程不安全的。
这是因为 INSTANCE = new Singleton(); 这个代码的字节码是 4 个。
1 | 17: new #3 // class test/Singleton |
17表示创建对象,将对象引用入栈20表示复制一份对象引用,引用地址21表示利用一个对象引用,调用构造方法初始化对象24表示利用一个对象引用,赋值给 static INSTANCE
步骤 21 和 24 之间不存在数据依赖关系,而且无论重排前后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
考虑这样一个情况
t1刚进入,此时INSTANCE为null,其初始化对象- 由于重排问题,其先执行了
24,就是给INSTANCE付给了一个对象引用,但是,此时对象并没有创建 CPU轮训
- 由于重排问题,其先执行了
t2进入,发现INSTANCE有了对象引用,开始使用INSTANCE,但是,此时INSTANCE是null
所以,我们要进行修改
1 | public final class Singleton { |
使用 volatile 修饰 INSTANCE,这是因为 volatile 具有写屏障和读屏障。可以保证 INSTANCE = new Singleton(); 执行的有序性,使其按照 17 - 20 - 21 - 24 的顺序执行。