📜 了解 JMM(Java 内存模型)中的 Happens-Before 规则 🌟

在 Java 并发编程中,JMM(Java Memory Model)是非常重要的概念,它定义了线程如何与内存交互,以及如何确保线程之间的通信一致性。JMM 引入了 Happens-Before 规则 来约束操作之间的顺序关系,保证数据的可见性和一致性。

现在我们一起学习 JMM 中定义的几条主要的 Happens-Before 规则吧!


1️⃣ 什么是 Happens-Before 规则?

Happens-Before 是一种用于定义内存访问顺序的规则。

  • 如果操作 A “Happens-Before” 操作 B,那么:
    1. 操作 A 的结果对操作 B 可见(数据正确同步)。
    2. 操作 A 必须在操作 B 之前完成(时间和因果顺序)。

JMM 中的 Happens-Before 告诉我们哪些操作是线程安全、不会引发并发问题的。


2️⃣ JMM 中定义的主要 Happens-Before 规则

以下是 JMM 中几条重要的 Happens-Before 规则:

(1)程序顺序规则 📋

  • 同一个线程内,代码的语句按程序的书写顺序执行。
  • 即在线程中,语句 A 按照在代码中的位置发生,随后才是语句 B

例如:

int a = 10;        // 操作 A
int b = a * 2;     // 操作 B

这里,a = 10 Happens-Before b = a * 2,因为它写在前面。


(2)监视器锁规则 🔒

  • 锁的释放 Happens-Before 锁的获取。
  • 一个线程释放锁后,另一个线程获取该锁,另一个线程可以看到前一个线程对共享数据的修改。

例如:

synchronized (lock) { 
    x = 100;        // 主线程操作 A,释放锁
}

synchronized (lock) { 
    System.out.println(x);   // 另一个线程操作 B,获取锁
}

在这里,锁的释放 (操作 A) Happens-Before 另一个线程获取锁 (操作 B),因此,线程 B 将看到 x = 100


(3)启动线程规则 🚀

  • 主线程调用 Thread.start() Happens-Before 新线程的所有操作。
  • 这就是说,主线程中对变量的写操作在调用 Thread.start() 时会被新线程看到。

例如:

Thread t = new Thread(() -> System.out.println(sharedVar));
sharedVar = 42;
t.start(); // 主线程启动 t,这里 Happens-Before 新线程的打印逻辑

这里的 t.start() Happens-Before 新线程中对 sharedVar 的访问。


(4)线程终止规则 🛑

  • 线程的终止 Happens-Before 检测到线程结束的操作(Thread.join()Thread.isAlive())。
  • 如果主线程等待某个子线程完成(通过调用 join()),那么子线程完成后的所有操作对主线程可见。

例如:

Thread t = new Thread(() -> sharedVar = 42);
t.start();
t.join(); // 等待子线程结束
System.out.println(sharedVar); // 子线程终止后,这里能保证看到 sharedVar = 42

在这里,t.join() Happens-Before 主线程中的打印语句。


(5)中断规则 🌟

  • 对线程的 interrupt() 方法调用 Happens-Before 检测到该线程的中断事件(通过 isInterrupted() 或捕获 InterruptedException)。
  • 即中断信号发送比被检测中断发生得早。


(6)Volatile 规则

  • volatile 变量的写操作 Happens-Before 读操作。
  • 如果一个线程对 volatile 变量进行了写操作,另一个线程读取该变量会看到最新的值。

例如:

volatile int sharedVar = 0;

Thread t1 = new Thread(() -> sharedVar = 42); // 写入操作
Thread t2 = new Thread(() -> System.out.println(sharedVar)); // 读取操作

在这里,sharedVar 的写操作 Happens-Before sharedVar 的读操作,因此线程 t2 能看到线程 t1 修改后的值。