在单线程程序中,我们一般很少关心“可见性”这个问题。
例如:

int x = 0;
x = 1;
System.out.println(x);

这段代码的结果很显然就是1,这是因为所有操作都在一个线程中,前面写入的自然而然地会被后面读到。
但是如果是在多线程环境下,就没这么简单了。

一个线程写入了变量,另一个线程不一定马上能看到。甚至在没有正确同步的情况下,另一个线程可能一直看到旧值。这也是并发编程里很多诡异 bug 的来源。

这时就需要理解一个非常重要的概念:happens-before

Happens-Before 是什么?

happens-before 可以翻译成 先行发生关系

如果操作 A happens-before 操作 B,那么可以认为:

操作 A 的结果,对操作 B 是可见的。

注意,这里的“先行发生”并不只是现实时间上的先后顺序。

它更准确地说,是 Java 内存模型提供的一种保证:
如果 A happens-before B,那么 B 一定能够看到 A 对共享变量造成的影响。

换句话说,happens-before 解决的是两个问题:

  • 可见性:一个线程的写入,另一个线程能不能看到?
  • 有序性:编译器和 CPU 的重排序,会不会破坏我们期望的执行顺序?

一个没有 Happens-Before 的例子

class Demo {
    static boolean ready = false;
    static int number = 0;

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (!ready) {
                // wait
            }
            System.out.println(number);
        });

        t.start();

        number = 42;
        ready = true;
    }
}

我们的直觉上可能认为线程最终会打印 42。

但这段代码其实是有问题的。

因为主线程对 number 和 ready 的写入,与子线程对它们的读取之间,没有建立可靠的 happens-before 关系。

所以子线程可能看不到 ready = true,也可能看到 ready = true,却仍然读到旧的 number 值。

用 volatile 建立 Happens-Before

可以把 ready 用 volatile 修饰

class Demo {
    static volatile boolean ready = false;
    static int number = 0;

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (!ready) {
                // wait
            }
            System.out.println(number);
        });

        t.start();

        number = 42;
        ready = true;
    }
}

这时,ready = true 是一次 volatile 写。
子线程中的 while (!ready) 是 volatile 读。

Java 内存模型规定:

对一个 volatile 变量的写,happens-before 后续对这个变量的读。

因此,当子线程读到 ready == true 时,它不仅能看到 ready 的新值,也能看到在 ready = true 之前发生的普通写入,也就是:

number = 42;

所以这时打印 42 就有了内存模型层面的保证。

Happens-Before的八大原则        

1. 程序次序规则

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

int a = 1;
int b = a + 1;

2. 监视器锁规则

对一个锁的解锁,happens-before 后续对同一个锁的加锁。

synchronized (lock) {
    value = 1;
}

线程 A 释放 lock,线程 B 之后获得同一个 lock,那么 B 能看到 A 在同步块里的写入。

3. volatile变量规则

对一个 volatile 变量的写,happens-before 后续对这个变量的读。

volatile boolean ready = false;

ready = true;     // 写
if (ready) { }    // 读

4. 线程启动规则

对线程对象调用 start(),happens-before 该线程中的所有操作。

Thread t = new Thread(() -> {
    System.out.println("run");
});

t.start();

start() 之前的操作,对新线程可见。

5. 线程启动规则

线程中的所有操作,happens-before 其他线程检测到该线程已经终止。

常见方式包括:

t.join();

也可以是 Thread.isAlive() 返回 false。

6. 线程中断规则

对线程调用 interrupt(),happens-before 被中断线程检测到中断事件。

比如:

t.interrupt();

之后目标线程通过 isInterrupted()、interrupted() 或阻塞方法抛出 InterruptedException 感知到中断。

7. 对象终结规则

一个对象的构造方法执行完成,happens-before 它的 finalize() 方法开始执行。

不过现在 finalize() 已经过时,不建议在实际代码中依赖它。

8. 传递性规则

如果 A happens-before B,B happens-before C,那么 A happens-before C。

A -> B
B -> C
所以 A -> C

这是把多个同步关系串起来分析并发代码的基础。

Happens-Before 不是时间顺序

是最容易误解的一点。

happens-before 不是说操作 A 在现实时间上一定比操作 B 更早发生。

它说的是:
如果 A happens-before B,那么 Java 内存模型保证 B 能看到 A 的结果。

反过来说,如果没有 happens-before,即使 A 在现实时间上先执行,B 也不一定能看到 A 的结果。

这就是并发编程和单线程编程最大的区别之一。

总结

happens-before 是理解 Java 并发的核心概念。

它不是单纯的“谁先执行”,而是内存模型里的“谁对谁可见”。

判断一段多线程代码是否可靠时,可以问自己一个问题:

一个线程的写入,和另一个线程的读取之间,有没有建立 happens-before 关系?

如果有,代码才有可靠的可见性和顺序保证。

如果没有,那么程序看起来“多数时候正常”,但在高并发、不同 CPU、不同 JVM 优化下,就可能出现难以复现的问题。

简单来说:

happens-before 是并发程序里判断内存可见性的规则。
写多线程代码时,不要只相信代码顺序,要相信同步关系。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐