双重检测锁的最初形态是通过在方法声明的部分加上synchronized进行同步,保证同一时间调用方法的线程只有一个,从而保证 new Singlton()
的线程安全:
public class Singleton { private static Singleton instance; private Singleton() { } public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
这样做的好处是代码简单、并且JVM保证 new Singlton()
这行代码线程安全。但是付出的代价有点高昂:
所有的线程的每一次调用都是同步调用,性能开销很大,而且 new Singlton()
只会执行一次,不需要每一次都进行同步。
既然只需要在 new Singlton()
时进行同步,那么把 synchronized
的同步范围缩小呢?
public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
把 synchronized
同步的范围缩小以后,貌似是解决了每次调用都需要进行同步而导致的性能开销的问题。但是有引入了新的问题: 线程不安全,返回的对象可能还没有初始化。
深入到字节码的层面来看看下面这段代码:
instance = new Singleton() returen instance;
正常情况下JVM编译成成字节码,它是这样的:
step.1 new:开辟一块内存空间 step.2 invokespecial:执行初始化方法,对内存进行初始化 step.3 putstatic:将该内存空间的引用赋值给instance step.4 areturn:方法执行结束,返回instance
当然这里限定在正常情况下,在特殊情况下也可以编译成这样:
step.1 new:开辟一块内存空间 step.3 putstatic:将该内存空间的引用赋值给instance step.2 invokespecial:执行初始化方法,对内存进行初始化 step.4 areturn:方法执行结束,返回instance
步骤2和步骤3进行了调换:先执行步骤3再执行步骤2。
这种特殊情况称之为: 指令重排序
:CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。当然不是乱排序,重排序保证CPU能够正确处理指令依赖情况以保障程序能够得出正确的执行结果。
HappensBefore
:先行发生,是
举一个《深入理解Java虚拟机》上的例子:
//以下操作在线程A中执行 int i = 1; //以下操作在线程B中执行 j = i; //以下操作在线程C中执行 i = 2;
如果hb( i=1
, j=i
),那么可以确定变量j的值一定等于1。得出这个结论的依据有两个:
i=1
的结果可以被 j=i
观察到
如果线程C的执行时间在线程A和线程B之间,那么 j
的值是多少呢?答案是不确定!因为线程C和线程B之间没有HappensBefore的关系:线程C对变量的 i
的更改可能被线程B观察到也可能不会!
这些是“天然的”、JVM保证的HappensBefore关系:
重点介绍 程序次序规则
, 管程锁定规则
, volatile变量规则
, 传递性
,后面分析需要用到这四个性质:
public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { //1 synchronized (Singleton.class) { //2 if (instance == null) { //3 instance = new Singleton(); //4 new //4.1 invokespecial //4.2 pustatic //4.3 } } } return instance; //5 } }
经过上面的讨论,已经知道因为JVM重排序导致 代码4.2
提前执行了,导致后面一个线程执行 代码1
返回的值为false,进而直接返回了还没有构造好的instance对象:
|线程1|线程2 |
|--|--|
| 1 | |
| 2 | |
| 3 | |
| 4.1 | |
| 4.3 | |
| | 1 |
| | 5 |
| 4.2 | |
| 5 | |
通过表格,可能清晰看到问题所在:线程1代码4.3 执行后,线程2执行代码1读到了脏数据。要想不读到脏数据,只要证明存在hb(T1-4.3,T2-1)(T1-4表示线程1代码4,T2-1表示线程2代码1,下同),那么是否存在呢?很遗憾,不存在:
用HappensBefore分析,可以很清晰、明确看到没有volatile修饰的双重检测锁是线程不安全的。但,真的是这样的吗?
在第二部分,通过HappensBefore分析没有volatile修饰的双重检测锁是线程不安全,那只有用volatile修饰的双重检测锁才是线程安全的吗?答案是否定的。
用volatile关键字修饰的本质是想利用 volatile变量规则
,使得写操作(T1-4)HappensBefore读操作(T2-1),那只要另找一条HappensBefore规则保证即可。答案是 程序次序规则
和 管程锁定规则
先看代码:
public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { //1 synchronized (Singleton.class) { //2 if (instance == null) { //3 Singleton temp = new Singleton(); //4 temp.toString(); //5 instance = temp; //6 } } } return instance; //7 } }
在原有的基础上加了两行代码:
instance = new Singleton(); //4 Singleton temp = new Singleton(); //4 temp.toString(); //5 instance = temp; //6
为什么要这么做?
通过管程锁定规则保证执行到 代码6
时,temp对象已经构造好了。想一想,为什么?
管程锁定规则
执行流程可能是这样的: |线程1|线程2 | 线程3| |--|--| -- | | 1 | | | | | | 1| | 2 | || | 3 | || | 4 | || | 5 | || | 6 | || | | | 2| | | | 3| | | 1 | 7 | | | 7 || | 7 | ||
无论怎样执行,其他线程都能够观察到T1-6的写操作
内存屏障。
JVM在凡是有volatile、synchronized出现的地方都加了一道内存屏障:重排序时,不可以把内存屏障后面的指令重排序到内存屏障前面执行,并且会及时的将线程工作内存中的数据及时更新到主内存中,进而使得其他的线程能够观察到最新的数据