目录java
- 双重检测锁的演变过程
- 利用HappensBefore分析并发问题
- 无volatile的双重检测锁
双重检测锁的最初形态是经过在方法声明的部分加上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
同步的范围缩小之后,貌似是解决了每次调用都须要进行同步而致使的性能开销的问题。可是有引入了新的问题:线程不安全,返回的对象可能尚未初始化。
app
深刻到字节码的层面来看看下面这段代码:性能
instance = new Singleton() returen instance;
正常状况下JVM编译成成字节码,它是这样的:线程
step.1 new:开辟一块内存空间 step.2 invokespecial:执行初始化方法,对内存进行初始化 step.3 putstatic:将该内存空间的引用赋值给instance step.4 areturn:方法执行结束,返回instance
固然这里限定在正常状况下,在特殊状况下也能够编译成这样:code
step.1 new:开辟一块内存空间 step.3 putstatic:将该内存空间的引用赋值给instance step.2 invokespecial:执行初始化方法,对内存进行初始化 step.4 areturn:方法执行结束,返回instance
步骤2和步骤3进行了调换:先执行步骤3再执行步骤2。对象
这种特殊状况称之为:指令重排序
:CPU采用了容许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。固然不是乱排序,重排序保证CPU可以正确处理指令依赖状况以保障程序可以得出正确的执行结果。排序
HappensBefore
:先行发生,是
换句话说,能够经过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对象已经构造好了。想想,为何?
管程锁定规则
开始生效:保证当前线程必定可以观察到T1-6操做执行流程多是这样的:
线程1 | 线程2 | 线程3 |
---|---|---|
1 | ||
1 | ||
2 | ||
3 | ||
4 | ||
5 | ||
6 | ||
2 | ||
3 | ||
1 | 7 | |
7 | ||
7 |
不管怎样执行,其余线程都可以观察到T1-6的写操做
内存屏障。
JVM在凡有volatile、synchronized出现的地方都加了一道内存屏障:重排序时,不能够把内存屏障后面的指令重排序到内存屏障前面执行,而且会及时的将线程工做内存中的数据及时更新到主内存中,进而使得其余的线程可以观察到最新的数据
参考资料