happens-before规则

前言

Happens-Before规则主要有两个作用:

  • 一是解决数据竞争问题;
  • 二是为开发人员提供足够强的内存可见性。

一、概述

1.1 数据竞争

数据竞争就是指并发条件下的状态属性不同步而引发的读写不一致问题

现假设有两个线程A、B要对内存中的同一个变量进行访问,线程A要对这个变量执行写操作,线程B要对这个变量执行读操作,两个操作是同时进行的,此时若不加以限制,线程B读操作所得到的结果就有两种可能,且结果是不可预测的,这并不是开发人员希望看到的结果,在JMM(Java内存模型)设计之初就考虑到了这个问题,必须人为的指定读/写操作的执行顺序,Happens-Before规则应运而生。

1.2 重排序

重排序就是指实际应用中,编译器和处理器为了优化程序性能而对指令序列进行重新排序的手段

在JMM中,对于重排序采取了如下策略:

  • 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
  • 对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(允许)。

image-20230329145617670

二、Happens-Before规则

2.1 定义

Happens-Before规则在JRS-133中定义如下:

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

定义1向开发人员保证如果操作A happens-before 操作B,那么操作A的结果将对操作B可见,且操作A的执行顺序在操作B之前。

定义2对编译器和处理器进行约束,重排序必须在不改变程序执行结果(单线程条件下)的前提下进行。

2.2 具体规则

Happens-Before规则共包含以下八条,程序执行时会严格按照规则进行。

  • 程序顺序规则:如果程序中操作A在操作B之前,那么在线程中A操作将在B操作之前执行。
  • 监视器锁规则:在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行。
  • volatile变量规则:对volatile变量的写入操作必须在对该变量的读操作之前执行。
  • 线程启动规则:在线程上对Thread.Start的调用必须在该线程中执行的任何操作之前执行。
  • 线程结束规则:线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者从Thread.join中成功返回,或者在调用Thread.isAlive时返回false。
  • 中断规则:当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt调用之前执行(通过抛出InterruptedException,或者调用isInterrupted和interrupted)。
  • 终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成。
  • 传递性:如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么操作A必须在操作C之前执行。

在 Java 语言里面,Happens-Before 的语义本质上是一种可见性,A Happens-Before B 意味着 A 事件对 B 事件来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。例如 A 事件发生在线程 1 上,B 事件发生在线程 2 上,Happens-Before 规则保证线程 2 上也能看到 A 事件的发生。

简而言之,happens-before 向我们保证了在多线程环境中,上一个操作对下一个操作的有序性和操作结果的可见性。

三、示例

  • 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int x;
static Object m = new Object();

new Thread(()->{
synchronized(m) {
x = 10;
}
},"t1").start();

new Thread(()->{
synchronized(m) {
System.out.println(x);
}
},"t2").start();
  • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
1
2
3
4
5
6
7
8
9
volatile static int x;

new Thread(()->{
x = 10;
},"t1").start();

new Thread(()->{
System.out.println(x);
},"t2").start();

说明:volatile修饰的变量, 通过写屏障, 共享到主存中, 其他线程通过读屏障, 读取主存的数据。

  • 线程 start 前对变量的写,对该线程开始后对该变量的读可见
1
2
3
4
5
6
7
static int x;

x = 10;

new Thread(()->{
System.out.println(x);
},"t2").start();

说明:线程还没启动时, 修改变量的值, 在启动线程后, 获取的变量值, 肯定是修改过的。

  • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)
1
2
3
4
5
6
7
8
9
static int x;

Thread t1 = new Thread(()->{
x = 10;
},"t1");
t1.start();

t1.join();
System.out.println(x);

说明:主线程获取的x值, 是线程执行完对x的写操作之后的值。

  • 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过 t2.interrupted 或 t2.isInterrupted)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static int x;
public static void main(String[] args) {
Thread t2 = new Thread(()->{
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println(x); // 10, 打断了, 读取的也是打断前修改的值
break;
}
}
},"t2");
t2.start();

new Thread(()->{
sleep(1);
x = 10;
t2.interrupt();
},"t1").start();

while(!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x); // 10
}
  • 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
  • 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子
1
2
3
4
5
6
7
8
9
10
11
12
volatile static int x;
static int y;

new Thread(()->{
y = 10;
x = 20;
},"t1").start();

new Thread(()->{
// x=20 对 t2 可见, 同时 y=10 也对 t2 可见
System.out.println(x);
},"t2").start();

说明

因为x加了volatile, 所以在volatile static int x 代码的上面添加了读屏障, 保证读到的x和y的变化是可见的(包括y, 只要是读屏障下面都可以); 通过传递性, t2线程对x,y的写操作, 都是可见的。


happens-before规则
http://example.com/2023/03/29/happens-before规则/
作者
程序员小魏
发布于
2023年3月29日
许可协议