happens-before 原则

发布时间 2023-07-02 15:51:05作者: 旅途的痕迹

happens-before 简述

从 JDK 5 开始,Java 使用新的 JSR-133 内存模型。JSR-133 使用 happens-before 的概念来阐述操作之间的内存可见性。在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。《JSR-133:Java Memory Model and Thread Specification》对 happens-before 关系的定义如下。

  • 第一点,如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  • 第二点,两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before 关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM 允许这种重排序)。

以上两点定义看上去是不是会有点矛盾?如果 A happens-before B,第一点说A的操作顺序必须排在B之前。第二点说两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before 关系来执行的结果一致,是可以重排序的,也就是说先执行A再执行B与先执行B再执行A的结果一致(要符合as-if-serial语义),B可以在A之前执行。那岂不是违反了第一条的第一个操作的执行顺序排在第二个操作之前?
这里举个例子

double pi = 3.14; // A
double r = 1.0; // B 
double area = pi * r * r; // C

根据happens-before原则,以上代码存在

A happens-before B
B happens-before C
A happens-before C (根据happens-before的传递性推导出来的,之后会讲述)

这里 A happens-before B,但实际上执行时B可以排在A之前执行,因为A和B之间并没有依赖关系,重排序的执行结果并不会对程序的执行造成影响(符合as-if-serial语义)。这是不是造成了以上两个定义的矛盾?其实并不矛盾,以上的第一点是JMM对程序员的承诺从程序员的角度来说,可以这样理解 happens-before 关系:如果 A happens-before B,那么 Java 内存模型将向程序员保证——A操作的结果将对 B 可见,且 A 的执行顺序排在 B 之前。注意,这只是 Java 内存模型向程序员做出的保证!实际上JMM 其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序正确同步的多线程程序),编译器和处理器怎么优化都行。JMM 这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。

happens-before 规则

《JSR-133:Java Memory Model and Thread Specification》定义了如下 happens-before 规则。

  1. 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
    如同上面的A happens-before B,B happens-before C
  2. 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
public class Test {
    public int i = 0;

    public synchronized void writer(){  //1
        i++;                            //2
    }                                   //3

    public synchronized int reader(){   //4
        return i;                       //5
    }                                   //6

}
/**
 * 假设线程 A 执行 writer()方法,随后线程 B 执行 reader()方法。根据 happens-before规则,这个过程包含的 happens-before 关系可以分为 3 类。
 * 根据程序次序规则,1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happens-before 6。
 * 根据监视器锁规则,3 happens-before 4。
 * 根据 happens-before 的传递性,2 happens-before 5。
 */
  1. volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个volatile 域的读。
class VolatileExample {
    int a = 0;
    volatile boolean flag = false;
    public void writer() {
        a = 1; // 1
        flag = true; // 2
    }
    public void reader() {
        if (flag) { // 3
            int i = a; // 4
        }
    }
}
//假设线程 A 执行 writer()方法之后,线程 B 执行 reader()方法。根据 happens-before规则,这个过程建立的 happens-before 关系可以分为 3 类:
//根据程序次序规则,1 happens-before 2;3 happens-before 4。
//根据 volatile 规则,2 happens-before 3。
//根据 happens-before 的传递性规则,1 happens-before 4。
  1. 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
  2. start()规则:如果线程 A 执行操作 ThreadB.start()(启动线程 B),那么 A 线程的ThreadB.start()操作 happens-before 于线程 B 中的任意操作。
public class ThreadExample {
    public static int i = 0;

    public static void threadBStart(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                int j = i; //4
            }
        },"B").start(); //3
    }

    public static void main(String[] args) {
        i++; // 1
        threadBStart(); //2
    }
}
//main线程开始,这个过程建立的 happens-before 关系可以分为 3 类:
//根据程序次序规则,1 happens-before 2; 3 happens-before 4
//根据start()规则,2 happens-before 3; 2 happens-before 4;
//根据 happens-before 的传递性规则,1 happens-before 4。
  1. join()规则:如果线程 A 执行操作 ThreadB.join()并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join()操作成功返回。
public class JoinExample {
    public static int i = 0;

    public static Thread threadBStart(){
        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                i ++;
            }
        }, "B");
        threadB.start();
        return threadB;
    }

    public static void main(String[] args) throws InterruptedException {
        threadBStart().join();
        int a = i;
    }
}

//线程b的所有操作happens-before main线程 join方法返回后

参考

《Java并发编程的艺术》