happens-before规则如何覆盖单变量的读写及内存可见性问题?
理解happens-before规则:从内存顺序到单变量可见性
一、happens-before与多变量内存重排(Dekker算法示例)
我把happens-before规则看作是处理**内存顺序(memory ordering)以及判断某种内存顺序是否符合程序顺序(program order)**的核心规则。以经典的Dekker算法场景为例:
public class DekkerMemoryReorderingExample { private int x; private int y; public void T1() { x = 1; int r1 = y; } public void T2() { y = 1; int r2 = x; } }
在缺乏同步机制的情况下,完全可能观察到结果 (r1, r2) = (0, 0)。无论底层是指令调度、CPU指令重排还是缓存导致的写入传播延迟,这种现象都属于StoreLoad内存重排,等价于以下执行顺序:
1. r1 = y 2. r2 = x 3. x = 1 4. y = 1
二、单变量场景下的happens-before与可见性问题(不安全计数器示例)
当场景简化到单个变量时,happens-before规则如何作用于内存顺序?我们来看这个不安全计数器的例子:
public class UnsafeCounter { private int counter = 0; public synchronized int increment() { counter++; } public int get() { return counter; } }
这个计数器存在明显问题:读取counter时没有持有与写入操作相同的锁,因此无法保证读取操作能看到之前所有的写入结果。从内存模型的正式定义来说,increment()中的写入操作与get()中的读取操作之间不存在happens-before关系。
三、从内存模型抽象层面解释单变量可见性问题
虽然这个场景没有多变量关联,但我们依然可以从内存模型的核心抽象规则来拆解:
- 程序顺序与内存顺序的脱节:对于单个线程来说,
increment()中的counter++(本质是读-改-写三步操作)遵循程序顺序,但由于get()未同步,JVM没有义务保证其他线程的读取操作能观察到这个线程的写入顺序。 - 内存屏障的缺失:
synchronized修饰的increment()会在退出时插入必要的内存屏障(比如StoreStore + StoreLoad),确保该线程的写入操作对后续持有同一锁的线程可见。但get()没有进入同步块,也就不会触发对应的LoadLoad/LoadStore屏障,无法强制从主内存刷新最新值,也无法禁止读取操作被重排到写入操作之前(从其他线程的视角看)。 - 内存重排的可能性:从内存模型的抽象分类来看,这里可能发生的重排包括:
- StoreLoad重排:其他线程的读取操作(
get()中的return counter)可能被重排到当前线程写入counter的操作之前,导致读取到旧值; - 读取操作无同步约束时,JVM允许将
get()中的读取操作缓存到寄存器或本地缓存中,后续多次调用get()都直接使用缓存值,完全不感知主内存的更新,这本质上是内存模型允许的"读取重排"(逻辑上等价于读取操作被提前并重复执行)。
- StoreLoad重排:其他线程的读取操作(
- happens-before的核心约束:JMM的happens-before规则明确:只有当两个操作之间存在明确的happens-before链时,才能保证可见性和顺序性。在这个例子中,
increment()的写入和get()的读取之间没有通过锁、volatile或其他同步机制建立happens-before关系,因此JMM允许任何符合单线程程序顺序的内存执行顺序,包括读取操作看不到最新写入值的情况。
内容的提问来源于stack exchange,提问作者TwITe
相关产品推荐
相关产品推荐

