happens-before原则是JMM的核心概念,happens-before原则是JMM用来规范Java虚拟机和CPU指令重排序以提供性能。happens-before原则的目的是保证程序的可见性,定义了变量如何从主内存中读到高速缓存,或者何时从高速缓存刷新到主内存中。在Java中happens-before原则主要围绕Volatile和synchronized关键字实现的。
下面来介绍一下happens-before原则的八个具体原则。
1、程序次序原则
在同一个线程中,按照代码的编写顺序,前面的操作happens-before与后面的任意操作。
举例:
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
//x的值是多少呢?
}
}
}
例子中,在一个线程执行中,x = 42一定会在v = true之前执行,即同一个线程中,程序在前面对某个变量的修改一定是对后续操作是可见的。
2、volatile变量规则
对同一个volatile变量的写操作,happens-before于后续对这个变量的读操作。
3、传递规则
如果A happens-before B,B happens-before C,则A happens-before C。
还是以这个demo举例,同一时刻有两个线程,线程A调用writer方法,线程B调用reader方法,那问题来了,线程B调用reader方法时,读取到的变量x的值是多少呢?(42)
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
//x的值是多少呢?
}
}
}
过程如下图所示:
参照前三条规则,我们可以分析出以下过程:
- 对于线程A,x = 42 happens-before 写变量 v = true,这是“程序次序原则”规定的;
- 线程A中的写变量 v = true happens-before 线程B中读取变量v = true,这是“volatile变量规则”规定的;
- 对于线程B,读取变量 v = true happens-before 读取变量x,这是“程序次序原则”规定的;、
- 根据“传递规则”,线程A中 x = 42 happens-before 线程B中读取变量x。
因此得出结论:线程B中读取变量x时,变量x的值为42。1.5 版本的并发工具包(java.util.concurrent)就是靠 volatile 语义来搞定可见性的。
4、锁定规则
对一个锁的解锁操作 happens-before 后续对和这个锁的加锁操作。
举例:
public class Test{
private int x = 0;
public void initX{
synchronized(this){ //自动加锁
if(this.x < 10){
this.x = 10;
}
} //自动释放锁
}
}
我们可以这样理解这段程序:假设变量x的值为10,线程A执行完synchronized代码块之后将x变量的值修改为10,并释放synchronized锁;当线程B进入synchronized代码块时,能够获取到线程A对x变量的写操作,也就是说,线程B访问到的x变量的值为10。
这一点也是synchronized关键字保证可见性的原因。
5、线程启动规则
如果线程A调用线程B的start()方法来启动线程B,则线程A的start()操作 happens-before 线程B中的任意操作。
我们也可以这样理解线程启动规则:线程A启动线程B之后,线程B能够看到线程A在启动线程B之前的所有操作。
举例:
// 在线程A中初始化线程B
Thread threadB = new Thread(()->{
// 此处的变量x的值是多少呢?答案是100
});
//线程A在启动线程B之前将共享变量x的值修改为100
x = 100;
// 启动线程B
threadB.start();
上述代码是在线程A中执行的一个代码片段,根据【原则五】线程的启动规则,线程A启动线程B之后,线程B能够看到线程A在启动线程B之前的操作,在线程B中访问到的x变量的值为100。
6、线程终结规则
线程A等待线程B完成(在线程A中调用线程B的join方法实现),当线程B完成后(线程A调用线程B的join方法返回),则线程A能够访问到线程B对共享变量的操作。
举例:
Thread threadB = new Thread(()-{
// 在线程B中,将共享变量x的值修改为100
x = 100;
});
// 在线程A中启动线程B
threadB.start();
// 在线程A中等待线程B执行完成
threadB.join();
// 此处访问共享变量x的值为100
例子中,在主线程中调用线程B中的start方法执行线程B中的逻辑,将x = 100,主线程中调用threadB.join(),主线程待线程B执行完毕后,可以读取到线程B中将x = 100的变化,即这个x = 100的操作对后面的主线程是可见的。
7、线程中断规则
对线程interrupt方法的调用 happens-before 被中断线程的代码检测到中断事件的发生。
举例:
// 在线程A中将x变量的值初始化为0
private int x = 0;
public void execute() {
// 在线程A中初始化线程B
Thread threadB = new Thread(()->{
// 线程B检测自己是否被中断
if (Thread.currentThread().isInterrupted()){
// 如果线程B被中断,则此时X的值为100
System.out.println(x);
}});
// 在线程A中启动线程B
threadB.start();
// 在线程A中将共享变量X的值修改为100
x = 100;
// 在线程A中中断线程B
threadB.interrupt();
}
例子中,线程A中断线程B之前将共享变量x的值由0改成100,当线程B中检测到中断时间时,此时线程B访问到的共享变量x的值是100,即线程A的写操作是对线程B是可见的。
8、对象终结原则
一个对象的初始化完成 happens-before 它的finalize()方法的开始。
举例:
public class TestThread {
public TestThread(){
System.out.println("构造方法");
}
@Override
protected void finalize() throws Throwable {
System.out.println("对象销毁");
}
public static void main(String[] args){
new TestThread();
System.gc();
}
}
运行结果如下所示:
构造方法
对象销毁
9、其他
提到 happens-before 原则时经常会与 as - if - serial原则做比较,这里介绍一下 as - if - serial原则,as-if-serial语句规定重排序要满足以下两个规则:
- 在单线程环境下不能改变程序执行的结果;
- 存在数据依赖关系代码(指令)片段的不允许重排序。
比如下面的代码:
// ①
int a = 1;
// ②
int b = 2;
// 依赖于 ① 和 ②
int c = a + b;
return c;
可能会被优化成:
// ②
int b = 2;
// ①
int a = 1;
// 依赖于 ① 和 ②
int c = a + b;
return c;
而不会优化成:
// 依赖于 ① 和 ②
int c = a + b;
// ②
int b = 2;
// ①
int a = 1;
return c;