07 happens-before规则对重排序的约束
Diego38 111 1

1. 前言

我们学习了内存屏障以及带有内存屏障指令的 final 关键字、volatile 关键字,在写代码或者 review 其他人代码时,是否都需要想象着将内存屏障指令一一呈现在代码之上,进而来判断是否有可见性问题?答案是不需要的,JMM 为我们提供一套规则–happens-before 规则,这种规则可以帮忙我们免去理解复杂的重排序,我们只需要理解并遵守这套规则(就像运用数学公理一样),写代码就可以了。

2. happens-before 规则定义

happens-before (先行发生) 最早来自一篇论文,作者使用 happens-before 来定义分布式系统之间的关系。happens-before 是一套规则,通过这套规则,可以使得程序员能轻松判断变量在多线程场景下是否可见的。

满足 happens-before 法则,那么就可以保证在多线程中是可见性的,是正确的操作

happens-before 规则可分为以下几种:

  • 程序顺序规则: 同一个线程中的每个操作都先行发生于 (happens-before) 出现在其后的任何一个操作
  • 同步器规则: 对一个同步器 (Synchonized) 的解锁先行发生于每一个后续对同一个同步器的加锁
  • volatile 规则: 对 volatile 字段的写入操作先行发生于每一个后续的该字段的读操作
  • 线程启动规则: Thread.start () 的调用会先行发生于启动线程里面的动作
  • 线程存活规则 Thread 中的所有动作都先行发生于其他线程检查到此线程结束或者 Thread.join()中返回或者 Thread.isAlive ()==false
  • 线程中断规则 一个线程 A 调用另一个线程 B 的 interrupt()都先行发生于线程 A 发现 B 被 A 中断( B 抛出异常或者 A 检测到 B 的 isInterrupted()或者 interrupted () )
  • 构造函数规则:一个对象构造函数的结束先行发生与该对象的 finalizer 的开始
  • 传递性规则: 如果 A 动作先行发生于 B 动作,而 B 动作先行发生于 C 动作,那么 A 动作先行发生于 C 动作。

读起来比较晦涩,大家不要着急,我们先看 happens-before 的对编译器和处理器的要求是什么,看完我们再回头看下以上的规则

  • 前一个操作的结果对后一个操作可见
  • 前一个操作指令排在第二个操作指令之前 基于这两点,我们再对同步器规则进行通俗解释即:

对一个同步器 (Synchonized) 的解锁之前的写对于后续加锁后的读是可见的;

对一个同步器 (Synchonized) 的解锁都先于后续对同一个同步器的加锁执行。

以此类推,对 volatile 规则的通俗解释就是:

对 volatile 字段的写入操作对于每一个后续的同一个字段的读操作都可见 对 volatile 字段的写操作会先于每一个后续的同一个字段的读操作执行。

7 条 happens-before 规则中,我们应该重点记忆单线程规则、监视器规则、volatile 规则、传递性规则。

2.1 happens-before 的规则使用

八条规则中单线程的程序顺序规则我们都理解,编译器和处理器遵守 as-if-serial, 所有指令按照代码编写顺序执行,单线程内不会出现可见性问题。

第二条监视器规则指的是:如果同一同步器解锁发生在同一同步器加锁之前,那么解锁之前的写操作对加锁之后的操作都可见。

我们看一段代码,通过运用程序顺序规则 + 同步器规则 + 传递性规则来判断可见性

public class HappensBefore {

    int a = 0;
    int b = 1;
    boolean flag = false;
    final Object lock = new Object();
    public  void write() {

        b ++; a = 1;  // 1 赋值操作
        synchronized (lock) { //2. 加锁
            flag = true; //3. 赋值
        } //4. 解锁

    }

    public void read() {
        synchronized (lock) { //5. write解锁之后执行加锁
            if (flag) { //6. 读取 正确的值true
                System.out.println(a);  //7. 读取a, 这里输出正常值1
            }
        }//8. 解锁
        System.out.println(b);//9 读取b, 这里输出正确值2
    }
}
  1. 程序顺序规则: 1 happensbefore 2, 2 happens-before 3 和 4; 5 happens-before 6,7,8,9
  2. 同步器规则: 4 happens-before5
  3. 传递性规则: 1234 happens-before 56789 结论,变量 a 和变量 b 是可见的。

我们再回顾下上节 volatile 语义的测试代码,

public class VolatileTest2 {
    int a = 0;
    int b = 1;
    volatile boolean flag = false;

    public void write() {
        b ++;        //1. 执行b++
        a = 1;       //2. 赋值为1
        flag = true; //3. 赋值flag, volatile写Happensbefore volatile读
    }

    public void read() {
        if (flag) {     //4. 读取flag正常, volatile写Happensbefore volatile 读取flag的值,禁止下面的读写操作与当前volatile读重排序
            System.out.println(a); //5
        }
        System.out.println(b);//6 读取b, 这里输出正确值2, 
    }
}

以上代码实现同样效果,同样满足 happens-before 规则。

  1. 程序顺序规则: 1 happensbefore 2, 2 happens-before 3
  2. 同步器规则: 3 happens-before 4
  3. 传递性规则: 12 happens-before 56 通过 volatile 的 Happensbefore 规则,我们同样可以判断出 ab 被写入后是在其他线程是可见的。

以下这段代码留给大家来分析,提示:基于线程启动 happensbefore 规则来判断

public class HappensBeforeThread {

    int a = 0;
    int b = 1;
    boolean flag = false;
    public  void write() {
        b ++;
        a = 1;
        new Thread(() -> {
            if (flag) {
                System.out.println(a + b);
            }
        } ).start();
    }

}

从以上几个例子我们可以完善上节 volatile 的可见性传递图形来表示,方便记忆 image

3. happens-before 与 JMM 的关系

前文提到 happens-before 简单易懂,是方便程序员基于规则来写出线程安全的代码。 JMM 遵守 happensbefore 规则,即如果一个操作执行结果需要对另外一个操作可见,那么这两个操作之间必然要存在 happensbefore 关系,而 JMM 是通过要求编译器和处理器禁止这种重排序来实现的。 image

  1. 总结 在 Java 语言中,happens-before 语义本质是判断可见性的依据,我们只需要理解 happens-before 规则,就可以编写出并发安全的代码。而 JMM 实现中会遵守 Happens-before 规则会禁止指令的重排序。 image
预览图
评论区

索引目录