Java内存模型
Java内存模型就是围绕在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的。Java内存模型定义了8种内存访问操作以及其执行时需要满足的诸项规则,以及对volatile的特殊规定,这些足以判定哪些内存操作在并发下是安全的。当然,我们并不需要以上述的方式来思考并发问题,我们只需要用先行发生原则(Java内存模型定义的一个等效判断原则)来确定一个操作在并发环境下是否安全即可。
先行发生原则
先行发生(Happens-before)原则指的是什么?
A Happens-before B就意味着A产生的影响可以被B观察到,也就是说A对B是可见的。
Java内存模型下的Happens-before原则:
程序的顺序性规则: 在一个线程中,按照程序的控制流顺序,前面的操作Happens-Before于后续的任意操作。
volatile变量规则:对一个volatile变量的写操作,Happens-Before于后续对这个变量的读操作。(后续是指时间上的先后)
传递性规则:如果A Happens-Before B,且B Happens-Before C,那么 A Happens-Before C。
管程中锁的规则:一个锁的解锁Happens-Before于后续这个锁的加锁。(后续是指时间上的先后)
线程start()规则:主线程A启动线程B的start()方法(即在线程A中启动线程B),那么该start()操作Happens-Before于线程B中的任意操作。
线程join()规则:主线程A等待子线程B完成(主线程A调用子线程B的join()方法),当子线程B完成后,主线程能够看到子线程的操作(指的是对共享变量的操作)。
线程中断规则:对线程interrupt()方法的调用先行发生与被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测是否发生中断。
对象终结规则:一个对象的初始化的完成,也就是构造函数执行的结束一定Happens-Before它的finalize()方法。
注意:先行发生(Happens-Before)原则与时间先后顺序没有因果关系,我们衡量并发问题时要以Happens-Before原则为准。
volatile
volatile关键字可以说是最轻量级的同步机制,可以用来解决可见性问题,以及用来保证部分程序的有序性。
volatile解决可见性问题
volatile最原始的意义就是禁用 CPU 缓存,这意味着对于volatile修饰的变量,对于其读写,都必须从内存中读取或写入,而不能使用CPU缓存。
从volatile的含义可以得知使用volatile可以有效的解决可见性问题。在之前的可见性问题的例子中,只需要对stop变量添加volatile关键字即可,代码如下。
VisibilityTest.java
public class VisibilityTest {
public static void main(String[] args) throws Exception {
VisibilityThread v = new VisibilityThread();
v.start();
//停顿1秒等待新启线程执行
Thread.sleep(1000);
System.out.println("即将置stop值为true");
v.stopIt();
Thread.sleep(1000);
System.out.println("finish main");
System.out.println("main中通过getStop获取的stop值:" + v.getStop());
}
}
VisibilityThread.java
public class VisibilityThread extends Thread{
private volatile boolean stop = false;
@Override
public void run() {
int i = 0;
System.out.println("start loop.");
while(!getStop()) {
i++;
}
System.out.println("finish loop,i=" + i);
}
public void stopIt() {
stop = true;
}
public boolean getStop(){
return stop;
}
}
volatile解决有序性问题
另外,volatile还可以在一定程度上解决有序性问题。
我们关注的有序性自然不是时间上的执行先后顺序,而是前序操作对于后续操作的影响,这就与Happens-before原则不谋而合。A先于B,这种有序性的实质就是保证A所产生的影响对B是可见的。
volatile解决有序性问题依赖的就是上述提到的Happens-before规则中的前3条规则,下面通过代码来具体分析volatile是如何利用这三条规则来保证有序性的。
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v) {
// 这里x会是多少呢?
System.out.println(x);
}
}
在上述代码,x会取到什么反常的值(没有volatile的情况下)?(x有可能等于0,这是为什么?)
这自然是因为编译优化带来的指令重排,导致了v = true操作先于x = 42执行(时间上先后),而reader()操作正好在两者之间执行,这样就可能出现x=0的情况。解决这个问题只需要用volatile对v进行修饰即可。使用volatile修饰后,再来分析这段代码。
首先看Happens-Before规则的第二条(对一个volatile变量的写操作,Happens-Before于后续对这个变量的读操作),这里需要注意的是怎么确定volatile变量操作的时间前后顺序。
- 通过赋值+值的判断来确定
- 如上述代码片段中(已加volatile关键字),writer()中对v进行了赋值,reader()中判断v的值,当判断v值确实是true后,则说明v的写操作在时间上先于其读操作发生。这种情况下,v = true Happens-Before if(v)。
之后结合Happens-Before规则的第一条,可以得出
- x = 42 Happens-Before v = true
- v = true Happens-Before if(v)(判断为true时)
- if(v) Happens-Before System.out.println(x)
再结合Happens-Before规则的第三条,可以得出
- x = 42 Happens-Before System.out.println(x)
- 所以此处x取值只能是42。
总结
- volatile可以解决可见性问题。
- volatile可以解决部分有序性问题,解决思路:
- 确定在不同线程X和Y中具有依赖关系的操作A和B的先后顺序(如A Happens-Before B)
- 取一个共享的布尔(也可以是其他类型)变量flag并赋初值
- 线程X中在A操作后改变flag的值
- 线程Y在B操作前判断flag的值是否为上一步改变的值
- 这样就可以保证具有依赖关系的A和B操作有序执行