JUC
JMM 规范下,多线程先行发生原则之 happens-before

概述

在 JMM 中, 如果一个操作执行的结果需要对另一个操作可见性或者代码重排序,那么这两个操作之间必须存在 happens-before 关系。

XY 案例说明

操作线程
x = 5线程 A 执行
y = x线程 B 执行
上述称为:写后读

问题:y 是否等于 5 呢?

  • 如果线程A的操作(x = 5)happens-before(先行发生)线程B的操作(y = x),那么可以确定线程B执行后y = 5 一定成立;
  • 如果他们不存在happens-before原则,那么 y = 5 不一定成立。

这就是 happens-before 原则的包含可见性和有序性的约束

先行发生原则说明

如果 Java 内存模型中所有的有序性都仅靠 volatilesynchronized 来完成,那么代码将会显得很臃肿。但实际我们没有时时、处处、次次,添加 volatilesynchronized 来完成程序,这是因为 Java 语言中 JMM 原则下有一个“先行发生”(Happens-Before)的原则限制和规矩

它是判断数据是否存在竞争,线程是否安全的非常有用的手段。依赖这个原则,我们可以通过几条简单规则一揽子解决并发环境下两个操作之间是否可能存在冲突的所有问题,而不需要陷入 Java 内存模型苦涩难懂的底层编译原理之中。

happens-before 总原则

  • 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见, 而且第一个操作的执行顺序排在第二个操作之前。
  • 两个操作之间存在 happens-before 关系,并不意味着一定要按照 happens-before 原则制定的顺序来执行。 如果重排序之后的执行结果与按照 happens-before 关系来执行的结果一致,那么这种重排序并不非法

happens-before 八条规则

  • 次序规则:在同一个线程中,按照程序代码顺序,前面的操作 happens-before 后面的操作。

  • 锁定规则:对一个监视器锁的解锁 happens-before 后续对这个监视器锁的加锁

  • volatile 变量规则:对一个 volatile 字段的写操作 happens-before 后面对这个字段的读操作。

  • 传递规则:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。

  • 线程启动规则(Thread Start Rule):Thread 对象的 start() 方法 happens-before 此线程的每一个动作。

  • 线程中断规则(Thread Interruption Rule):对线程 interrupt() 的调用 happens-before 被中断线程的代码检测到中断事件的发生。

  • 线程终止规则(Thread Termination Rule):线程中的所有操作 happens-before 对此线程的终止检测,我们可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值等手段检测线程是否已经终止执行。

  • 对象终结规则(Finalizer Rule):一个对象的构造函数执行、结束 happens-before 它的 finalize() 方法的开始(对象没有完成初始化之前,是不能调用 finalized() 方法的)。

happens-before 总结

在 Java 语言里面,Happens-Before 的语义本质上是一种可见行。A Happens-Before B 意味着 A 发生过的事情对 B 来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。

JMM 的设计分为两部分:

  • 一部分是面向编程人员提供的,也就是 happens-before 规则,它通俗易懂阐述了一个强内存模型,我们只要理解 happens-before 规则,就可以编写并发安全的程序;
  • 另一部分是针对 JVM 实现的,为了尽可能少的对编译器和处理器做约束从而提高性能,JMM 在不影响程序执行结果的前提下对其不做要求,即允许优化重排序。程序员只需要关注前者就要好了,也就是理解 happens-before 规则即可,其他繁杂的内容有 JMM 规范结合操作系统处理。

案例说明

代码示例

TestDemo.java
public class TestDemo {
    private int value = 0;
  
  	public void getValue() {
      return value;
    }
  
  	public int setValue() {
      return ++value;
    }
}

假设存在线程 A 和 B,线程 A 先(时间上的先后)调用了 setValue(),然后线程 B 调用了同一个对象的 getValue(),那么线程 B 收到的返回值是什么?

解释

我们就这段简单的代码一次分析 happens-before 的规则(规则 5,6,7,8 可以忽略,因为他们和这段代码毫无关系):

  • 由于两个方法是由不同的线程调用,不在同一个线程中,所以肯定不满足程序次序规则;
  • 两个方法都没有使用锁,所以不满足锁定规则;
  • 变量不是用 volatile 修饰的,所以 volatile 变量规则不满足;
  • 传递规则肯定不满足。

所以我们无法通过 happens-before 原则推导出线程 A happens-before 线程 B,虽然可以确认在时间上线程A优先于线程 B 指定,但就是无法确认线程 B 获得的结果是什么,所以这段代码不是线程安全的。那么怎么修复这段代码呢?

修复01

把 getter/setter 方法都定义为 synchronized 方法

TestDemo.java
public class TestDemo {
    private int value = 0;
  
  	public synchronized void getValue() {
      return value;
    }
  
  	public synchronized int setValue() {
      return ++value;
    }
}

修复02

把 value 定义为 volatile 变量,由于 setter 方法对 value 的修改不依赖 value 的原值,满足 volatile 关键字使用场景

TestDemo.java
public class TestDemo {
    private volatile int value = 0;
  
  	// 使用:把 value 定义为 volatile 变量,由于 setter 方法对 value 的修改不依赖 value 的原值,满足 volatile 关键字使用场景
  	// 理由:利用 volatile 保证读取操作的可见性;利用 synchronized 保证复合操作的原子性结合使用锁和 volatile 变量来减少同步的开销
  	public void getValue() {
      return value; // 利用 volatile 保证读取操作的可见行
    }
  
  	public synchronized int setValue() {
      return ++value; // 利用 synchronized 保证复合操作的原子性
    }
}