目录
一、Happens-Before模型简介
二、组成Happens-Before模型的八种规则
2.1 程序顺序规则(as-if-serial语义)
2.2 传递性规则
2.3 volatile变量规则
2.4 监视器锁规则
2.5 start规则
2.6 Join规则
一、Happens-Before模型简介
除了显示引用volatile关键字能够保证可见性以外,在Java中,还有很多的可见性保障的规则。
从JDK1.5开始,引入了一个happens-before的概念来阐述多个线程操作共享变量的可见性问题。所以我们可以认为在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作必须要存在 happens-before关系。这两个操作可以是同一个线程,也可以是不同的线程。
JMM可以通过happens-before关系向程序员提供跨线程的内存可见性,即如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作不在同一个线程中执行,但是JMM向程序员保证a操作对b操作可见。具体定义为:
- 如果一个操作happens-before另外一个操作(发生在另一个操作之前),那么第一个操作的执行结果对第二个操作可见,且第一个操作的执行顺序在第二个操作之前。
- 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。
上面两条规则,第一条,是JMM对程序员的保证。从程序员的角度可以这样理解happens-before:如果A happens-before B ,那么java内存模型将向程序员保证————A操作的结果一定对B操作可见,且A的执行顺序一定在B的前面。第二条是JMM对编译器和处理器重排序的约束原则,只要不改变程序的执行结果,编译器和处理器怎么优化都行。
二、组成Happens-Before模型的八种规则
happens-before模型是JMM要求多个操作必须满足的关系模型,而happens-before模型是由八条具体规则组成的,具体如下:
- 程序顺序规则(as-if-serial语义):一个线程中的每个操作,happens-before于该线程的任意后续操作。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对该volatile域的读。
- 传递性规则:如果A happens-before B,且B happens-before C,那么A happens-before C。
- start()规则: 如果线程A执行ThreadB.start(),那么A线程的ThreadB.start()操作happens-before于线程B的任意操作。
- join()规则: 如果线程A执行ThreadB.join(),那么B线程中的任意操作happens-before于线程A从ThreadB.join()成功返回。
- 程序中断规则:对线程interrupted()方法的调用happens-before与被中断线程的代码检测到中断时间的发生。
- 对象finalize规则:一个对象初始化完成(构造函数执行结束)happens-before于发生它的finalize()方法的开始。
2.1 程序顺序规则(as-if-serial语义)
- 不能改变程序的执行结果(在单线程环境下,执行的结果不变)
- 依赖问题, 如果两个指令存在依赖关系,不允许重排序
这样听起来好像happens-before和as-if-serial语义没什么区别,但是一定要搞清楚as-if-serial是组成happens-before模型的其中一条规则,两者是包含的关系,下面对两者进行一个比较:
- as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before保证正确同步的多线程和单线程的执行结果不被改变。
- as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
- as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。
int a=0;
int b=0;
void test(){
int a=1; a
int b=1; b
int c=a*b; c
}
a happens -before b ; b happens before c
2.2 传递性规则
a happens-before b 且 b happens- before c,那么 a happens-before c
2.3 volatile变量规则
volatile 修饰的变量的写操作,一定happens-before后续对于volatile变量的读操作。该规则利用volatile关键字通过内存屏障机制来防止指令重排。
上图中的No表示不允许重排序,由上图我们就知道了使用volatile可以保证有序性。
public class VolatileExample {
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; 1
flag = true; // 修改 2
}
public void reader() {
if (flag) { // true 3
// i的值最终一定会被设置为1
int i = a; 4
}
}
}
1 happens-before 2 -> 因为程序顺序规则和volatile规则。
3 happens-before 4 -> 因为程序顺序规则
2 happens-before 3 -> 因为volatile规则
1 happens-before 4 -> 因为传递性规则
基于上面的规则,保证了最终i=1成立。
这里重点讲一下上面的1 happens-before 2 为什么成立。在上面给出了volatile重排序规则表的图片,其中有一条是代码中第一个操作是普通读写(在上面代码中对应了a=1),第二个操作是volatile写(在上面代码中对应了flag=true),这种情况是不允许重排序的,所以1 happens-before 2成立。
2.4 监视器锁规则
int x = 10;
synchronized(this) {
// 后续线程读取到的x的值一定12
if(x < 12) {
x = 12;
}
}
x = 12;
2.5 start规则
public class StartDemo{
int x = 0;
Thread t1 = new Thread(()->{
// 读取x的值 一定是20
if(x == 20){
}
});
x = 20;
// t1.start()以及之前的所有操作都发生在t1线程中任意操作之前
t1.start();
}
2.6 Join规则
public class Test{
int x=0;
Thread t1=new Thread(()->{
// t1线程中的任意操作都发生在当前线程中t1.join成功返回之前
x = 200;
});
t1.start();
t1.join(); //保证结果的可见性。
//在此处读取到的x的值一定是200.
}
相关文章:
【Java内存模型】Java内存模型(JMM)详解以及并发编程的三个重要特性(原子性,可见性,有序性)
【并发编程】volatile关键字最全详解,看这一篇就够了
【并发编程】synchronized关键字最全详解,看这一篇就够了