【并发基础】Happens-Before模型详解

目录

一、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操作可见。具体定义为:

  1. 如果一个操作happens-before另外一个操作(发生在另一个操作之前),那么第一个操作的执行结果对第二个操作可见,且第一个操作的执行顺序在第二个操作之前。
  2. 两个操作之间存在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模型的其中一条规则,两者是包含的关系,下面对两者进行一个比较:

  1. as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before保证正确同步的多线程和单线程的执行结果不被改变。
  2. as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
  3. 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关键字最全详解,看这一篇就够了
 

猜你喜欢

转载自blog.csdn.net/cy973071263/article/details/130548186