这个例子非常经典。
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
的顺序执行。