Java Happens-Before 保证

更新于 2025-12-28

Jakob Jenkov 2023-08-02

Java 的 happens-before(先行发生)保证 是一套规则,用于规范 Java 虚拟机(JVM)和 CPU 在为了性能优化而对指令进行重排序时的行为。该保证使得线程能够依赖变量值何时从主内存同步到工作内存(或反之),以及在同步某个变量的同时,还有哪些其他变量也已被同步。

Java 的 happens-before 保证主要围绕对 volatile 变量的访问,以及在 synchronized 块内对变量的访问展开。

本教程将介绍 Java volatilesynchronized 提供的 happens-before 保证,但不会深入讲解这两个关键字的所有细节。相关内容可参考以下教程:

指令重排序(Instruction Reordering)

现代 CPU 具备并行执行不相互依赖的指令的能力。例如,以下两条指令彼此独立,因此可以并行执行:

a = b + c
d = e + f

但下面这两条指令则不能轻易并行执行,因为第二条依赖于第一条的结果:

a = b + c
d = a + e

假设上述两条指令属于一个更大的指令序列,如下所示:

a = b + c
d = a + e
l = m + n
y = x + z

这些指令可以被重排为:

a = b + c
l = m + n
y = x + z
d = a + e

这样,CPU 至少可以并行执行前三条指令;一旦第一条完成,即可开始执行第四条。

如你所见,指令重排序能提升 CPU 指令的并行执行能力,从而提高性能。

只要程序的语义不变(即最终结果与源代码顺序执行一致),JVM 和 CPU 就允许进行指令重排序。


多 CPU 计算机中的指令重排序问题

在多线程、多 CPU 系统中,指令重排序会带来一些挑战。下面通过一个代码示例来说明这些问题(请注意:此示例仅为说明问题,并非推荐实践)。

假设有两个线程协作尽可能快地在屏幕上绘制帧:

  • 一个线程负责生成帧(生产者)
  • 另一个线程负责绘制帧(消费者)

这两个线程需要通过某种通信机制交换帧。下面是一个名为 FrameExchanger 的通信类示例:

生产者线程尽可能快地生成帧,消费者线程则尽可能快地绘制这些帧。

有时,生产者可能在消费者绘制完前一帧之前就生成了两帧。此时,只应绘制最新的一帧——我们不希望消费者落后于生产者。如果生产者在上一帧尚未绘制时就生成了新帧,则直接覆盖旧帧(即“丢弃”旧帧)。

有时,消费者可能刚绘制完一帧就准备好绘制下一帧,但生产者尚未生成新帧。这时,消费者应等待新帧到来,而不是重复绘制相同的帧(这会浪费 CPU 和 GPU 资源)。

FrameExchanger 类统计已存储和已取出的帧数量,以便了解有多少帧被丢弃。

以下是 FrameExchanger 的代码(Frame 类定义省略,因其结构不影响理解):

public class FrameExchanger {
    private long framesStoredCount = 0;
    private long framesTakenCount = 0;
    private boolean hasNewFrame = false;
    private Frame frame = null;

    // 由帧生成线程调用
    public void storeFrame(Frame frame) {
        this.frame = frame;
        this.framesStoredCount++;
        this.hasNewFrame = true;
    }

    // 由帧绘制线程调用
    public Frame takeFrame() {
        while (!hasNewFrame) {
            // 忙等待,直到新帧到达
        }
        Frame newFrame = this.frame;
        this.framesTakenCount++;
        this.hasNewFrame = false;
        return newFrame;
    }
}

注意 storeFrame() 方法中的三条指令看起来互不依赖,因此 JVM 或 CPU 可能认为可以重排序它们以提升性能。

但如果指令被重排如下:

public void storeFrame(Frame frame) {
    this.hasNewFrame = true;       // 先设为 true
    this.framesStoredCount++;
    this.frame = frame;            // 后赋值
}

那么字段 hasNewFrame 会在 frame 字段被赋值之前就被设为 true。这意味着,如果绘制线程正在 takeFrame() 的 while 循环中等待,它可能会提前退出循环,并读取到旧的 Frame 对象,导致重复绘制旧帧,浪费资源。

虽然在此例中重复绘制旧帧不会导致程序崩溃,但在其他场景下,此类重排序可能导致程序逻辑错误。


Java volatile 的可见性保证

Java 的 volatile 关键字提供了一些可见性保证:对 volatile 变量的写入会立即同步到主内存,读取则直接从主内存加载。这种同步机制确保了变量值对其他线程可见。

volatile 写入的可见性保证

当你写入一个 volatile 变量时,该值会直接写入主内存。此外,当前线程可见的所有变量也会同步到主内存。

示例:

this.nonVolatileVarA = 34;
this.nonVolatileVarB = new String("Text");
this.volatileVarC = 300;  // volatileVarC 是 volatile 变量

当第三行写入 volatileVarC 时,前两个非 volatile 变量的值也会被同步到主内存。

volatile 读取的可见性保证

当你读取一个 volatile 变量时,该值会直接从主内存读取。此外,当前线程可见的所有变量也会从主内存刷新其值。

示例:

c = other.volatileVarC;   // 读取 volatile 变量
b = other.nonVolatileB;
a = other.nonVolatileA;

当读取 other.volatileVarC 时,nonVolatileBnonVolatileA 也会从主内存重新加载。


Java volatile 的 Happens-Before 保证

为了防止指令重排序破坏 volatile 的可见性保证,Java 引入了 volatile happens-before 保证,对 volatile 变量周围的指令重排序施加限制。

修改后的 FrameExchanger(使用 volatile)

hasNewFrame 声明为 volatile

public class FrameExchanger {
    private long framesStoredCount = 0;
    private long framesTakenCount = 0;
    private volatile boolean hasNewFrame = false;  // volatile
    private Frame frame = null;

    public void storeFrame(Frame frame) {
        this.frame = frame;
        this.framesStoredCount++;
        this.hasNewFrame = true;
    }

    public Frame takeFrame() {
        while (!hasNewFrame) { /* 忙等待 */ }
        Frame newFrame = this.frame;
        this.framesTakenCount++;
        this.hasNewFrame = false;
        return newFrame;
    }
}

现在,当 hasNewFrame = true 时,frameframesStoredCount 会同步到主内存;每次消费者线程读取 hasNewFrame 时,也会从主内存刷新其他变量。

但如果 JVM 将 storeFrame() 重排为:

public void storeFrame(Frame frame) {
    this.hasNewFrame = true;      // 先设 volatile 为 true
    this.framesStoredCount++;
    this.frame = frame;           // 后赋值
}

那么在 hasNewFrame 被设为 true 时,frame 还未被赋值!此时消费者可能读到未初始化或旧的 frame,导致错误。

volatile 写入的 Happens-Before 保证

在写入 volatile 变量之前的所有写操作(无论是否 volatile),都必须在该 volatile 写入之前完成。

因此,在 storeFrame() 中,前两条写入不能被重排到 hasNewFrame = true 之后。

但前两条之间可以自由重排(因为它们都不是 volatile):

this.framesStoredCount++;
this.frame = frame;
this.hasNewFrame = true;  // OK

这不会破坏语义,因为 frame 仍在 hasNewFrame 之前被赋值。

volatile 读取的 Happens-Before 保证

对 volatile 变量的读取,必须发生在其后所有读取(无论是否 volatile)之前。

示例:

int a = this.volatileVarA;      // volatile 读取
int b = this.nonVolatileVarB;
int c = this.nonVolatileVarC;

第二、三条读取不能被重排到第一条之前。但它们彼此之间可以重排:

int a = this.volatileVarA;
int c = this.nonVolatileVarC;  // OK
int b = this.nonVolatileVarB;

由于 volatile 读取会刷新所有可见变量,因此后续读取能获得最新值。

takeFrame() 中,while (!hasNewFrame) 是对 volatile 的读取,因此后续对 frame 等的读取不会被提前,保证了正确性。


Java synchronized 的可见性保证

synchronized 块也提供类似 volatile 的可见性保证。

synchronized 进入时的可见性保证

当线程进入 synchronized 块时,所有可见变量都会从主内存刷新

synchronized 退出时的可见性保证

当线程退出 synchronized 块时,所有可见变量都会写回主内存

示例:ValueExchanger

public class ValueExchanger {
    private int valA, valB, valC;

    public void set(Values v) {
        this.valA = v.valA;
        this.valB = v.valB;
        synchronized(this) {
            this.valC = v.valC;
        } // 退出时,valA/B/C 都写回主内存
    }

    public void get(Values v) {
        synchronized(this) {
            v.valC = this.valC;
        } // 进入时,所有变量从主内存刷新
        v.valB = this.valB;
        v.valA = this.valA;
    }
}

synchronized 块的位置很关键:

  • set() 中放在最后,确保所有修改在退出时同步到主内存。
  • get() 中放在最前,确保读取前从主内存获取最新值。

Java synchronized 的 Happens-Before 保证

synchronized 提供两个 happens-before 保证:

1. synchronized 块开始的 Happens-Before 保证

进入 synchronized 块前,不能有对该块内所用变量的读取被重排到块内之后。

get() 方法中,如果将读取 valBvalA 重排到 synchronized 块之前:

// ❌ 不允许的重排
v.valB = this.valB;
v.valA = this.valA;
synchronized(this) { ... }

那么 valBvalA 就无法获得主内存的最新值,破坏可见性保证。

2. synchronized 块结束的 Happens-Before 保证

在 synchronized 块内或之前的写入,不能被重排到块结束后。

set() 方法中,如果将 valAvalB 的写入放到 synchronized 块之后:

// ❌ 不允许的重排
synchronized(this) { this.valC = v.valC; }
this.valA = v.valA;
this.valB = v.valB;

那么 valAvalB 的修改就不会在退出同步块时写回主内存,其他线程可能看不到更新。