了解过Java并发编程知识的童鞋都知道,Java内存模型是围绕着并发过程中如何处理原子性、可见性和有序性3个特征来建立的,其中有序性最为复杂。
我们习惯性的认为代码总是从先到后、依次执行的,这在单线程的时候确实是没错的(至少程序是正确的运行的)。但在并发时,有时候给人感觉写在后面的代码,比写在前面的代码先执行,如同出现了幻觉。这就是鼎鼎大名的指令重排,指令重排是很有必要的,因为大大提高了cpu处理性能。
然而,指令重排,在提高了性能的同时,也会发生一些意想不到的灾难,举个栗子:
class UnsafeOrderExample {
int x = 0;
boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这里 x 会是多少呢?
System.out.print(x);
}
}
}
对于上面的代码,如果只有一个线程,先执行writer方法,然后再执行reader方法,会得到一个跟预期一致的结果是42。
但假设有两个线程A、B,两者同时分别执行writer和reader方法,最终reader会输出什么呢?很显然,答案是不固定的,有可能输出42,也有可能输出0;这是因为writer方法中的代码有可能发生指令重排,导致v=true有可能会发生在x=42之前。这个类是线程不安全类。
指令重排是必要的,但同时它又带来了一些麻烦,这可怎么办?别急,Java对此指定了Happens-Before规则,既然不能禁止指令重排,那就用规则对指令重排作约束,正所谓“爱,就是克制”嘛。
正如前面所说,虽然jvm和执行系统会对指令进行一定的重排,但也是建立在一些原则上的,并非所有指令都可以随便改变执行位置。这些原则就是Happens-Before原则。Happens-Before可以直译为“先行发生”,但其想表达的更深层的意思是“前面操作的结果对后续操作是可见的”。所以比较正式的说法是:Happens-Before约束了编译器的优化行为,虽然允许编译器优化,但是编译器优化后一定要遵循Happens-Before原则。
程序顺序原则,指的是在一个线程内,按照程序代码的顺序,前面的代码运行的结果能被后面的代码可见。(准确的说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。)
举个简单栗子:
int a,b
a=1
b=a+1 //如果指令重排不遵循程序顺序原则,则b有可能等于1
如果指令重排不遵循程序顺序原则,以上的代码的b最终有可能等于1,而不是我们期望的2。这个原则就保证了程序语义的正确性,重排指令不允许改掉原来的代码语义。
传递性,指的是如果A Happens-Before于B,B Happens-Before于C,则A Happens-Before于C。这个是很好理解的。用白话说就是,如果A的操作结果对B可见,B操作结果对C可见,则A的操作结果对C也是可见的。
指对一个volatile变量的写操作,Happens-Before于后续对这个volatile变量的读操作。如果单单是理解这句话的意思,就是我们熟悉的禁用cpu缓存的意思,使得volatile修饰的变量读到永远是最新的值。
如果这个规则跟第二个规则“传递性”结合来看,会有什么效果呢?我们可以通过改一下上面的例程来看看:
class UnsafeExample {
int x = 0;
volatile boolean v = false;//v用volatile修饰
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这里 x 会是多少呢?
System.out.print(x);
}
}
}
对比这段代码跟第一个例子中的代码,变化的只是成员变量v用了volatile修饰,如果仅仅是用“volatile变量规则”来看,如果同样是线程A、B同时分别调用writer和reader,得到的不也是有42或者0两个结果么?
别慌,如果我们再结合“传递性”规则来看:
根据“传递性”,可以得出x=42 Happens-Before 于读变量v=true,是不是恍然大悟了呢?由此可以得出最终B线程执行的reader方法输出的x=42而不是0。而这个结果,是靠“volatile变量规则”+“传递性”推导出来的,凭直觉是比较难看出来的。经过这样一番修改后,这个类就变成了线程安全了。
锁规则,指的是一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
举个栗子:
synchronized (this) { // 此处自动加锁
// x 是共享变量, 初始值 =10
if (this.x < 12) {
this.x = 12;
}
} // 此处自动解锁
假设线程A执行完synchronized代码块后,x的值变成了12,线程B进入代码块时,可以看到线程A对x的修改,也就是能读到x==12。这个比较容易理解。
指的是主线程A启动子线程B后,子线程B能看到主线程在启动线程B前的操作。
举个栗子:
Thread B = new Thread(()->{
// 主线程调用 B.start() 之前
// 所有对共享变量的修改,此处皆可见
// 此例中,var==77
});
// 此处对共享变量 var 修改
var = 77;
// 主线程启动子线程
B.start();
此处,线程B能读到var==77。
这个规则跟上一条规则有点类似,只不过这个规则是跟线程等待相关的。指的是主线程A等待子线程B完成(对B线程join()调用),当子线程B操作完成后,主线程A能看到B线程的操作。
举个栗子:
Thread B = new Thread(()->{
// 此处对共享变量 var 修改
var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程 B 可见
// 主线程启动子线程
B.start();
B.join()
// 子线程所有对共享变量的修改
// 在主线程调用 B.join() 之后皆可见
// 此例中,var==66
此处,主线程A能看到线程B对共享变量var的操作,也就是可以看到var==66。
指的是线程A调用线程B的interrupt()方法,Happens-Before 于线程B检测中断事件(也就是Thread.interrupted()方法)。这个也很容易理解。
指的是对象的构造函数执行、结束 Happens-Before 于finalize()方法的开始。
Happens-Before原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要一句,依靠这个原则,我们可以解决并发环境下两个操作之间是否存在冲突的所有问题。