bigywater.tech
Java 内部原理 · 93

happens-before 真实验证:没有volatile,reader线程真实卡在死循环里永远出不来

happens-before 是 JMM(Java内存模型)里最抽象的概念之一,这篇用最直接的方式验证它:有volatile时2000轮测试真实零错误,没有volatile时reader线程真实被优化成永远退不出的死循环——不是理论推测,是真实观察到的现象。

volatile保证真实2000/2000次正确
真实验证:volatile写happens-before volatile读,跨线程2000轮测试,reader真实0次读到错误的data值
无volatile时reader真实死循环
真实验证:去掉volatile后,reader线程的自旋计数器真实永远停在0,5轮测试全部真实2秒超时未能退出

上一篇:InheritableThreadLocal 真实验证

真实验证:volatile写读构成的happens-before边界真实生效

writer线程先写一个普通变量,再写一个volatile标志位;reader线程先自旋等volatile变true,再读普通变量——验证普通变量的值是否真实可见。

HappensBeforeDemo.java static int data = 0; static volatile boolean ready = false; static void writer() { data = 42; // 1. 普通写 ready = true; // 2. volatile写 } static void reader(AtomicInteger observed) { while (!ready) {} // 3. volatile读 observed.set(data); // 4. 真实:1 happens-before 4 }
真实实测
2000 轮真实测试,因happens-before保证,观察到data错误值(非42)的次数: 0(应为0)

真实结果验证了 JMM 里最重要的一条 happens-before 规则:对同一个 volatile 变量,写操作 happens-before 后续的读操作。这条规则真实带来的效果不只是"ready 这个变量本身可见",而是真实建立了一条完整的传递链:data = 42(普通写,在volatile写之前)→ ready = true(volatile写)→while(!ready) 读到 true(volatile读)→ 之后的 data 读取。因为 happens-before 具有传递性,volatile 写读之间的这条边界真实把 data 的可见性也"捎带"保证了,2000 轮跨线程测试真实做到零错误。

意外真实发现:去掉volatile,reader线程真实卡进永久死循环

同样的结构,把volatile去掉,改用普通boolean做标志位,看reader线程是否还能真实退出循环。

HappensBeforeDemo.java static boolean plainReady = false; // 没有volatile Thread reader = new Thread(() -> { int count = 0; while (!plainReady) { count++; } loopCount.set(count); });
真实实测(5轮独立测试)
第1轮无volatile测试: reader真实自旋次数=0, 真实是否在2秒内退出=false, 真实耗时=2023ms
第2轮无volatile测试: reader真实自旋次数=0, 真实是否在2秒内退出=false, 真实耗时=2029ms
第3轮无volatile测试: reader真实自旋次数=0, 真实是否在2秒内退出=false, 真实耗时=2025ms
第4轮无volatile测试: reader真实自旋次数=0, 真实是否在2秒内退出=false, 真实耗时=2029ms
第5轮无volatile测试: reader真实自旋次数=0, 真实是否在2秒内退出=false, 真实耗时=2026ms
真实结果比"理论上可能不可见"更极端。5 轮独立测试,reader 线程真实全部没能在 2 秒超时内退出循环——而且自旋计数器 count 真实永远停在 0,说明循环体压根没有真的重新执行过第二次(如果它老老实实每次都重新读一遍 plainReady 内存,至少会看到自己的 count 在增长)。这个现象印证了没有 volatile 修饰的普通变量真实完全没有可见性保证:JIT 编译器在编译这段循环时,真实判断出 plainReady 在循环体内没有被当前线程修改,于是把这次内存读取真实提升(hoist)到循环外面,只读一次并缓存进寄存器——之后的每次循环判断都真实只是比较寄存器里的旧值,永远不会再去主内存看 writer 线程真实做的修改。volatile 的关键作用之一,就是真实禁止编译器做这种优化,强制每次读取都真实穿透到主内存。

真实结论:happens-before 不是抽象概念,是真实可观察的运行时行为

这组对照实验最有价值的地方在于:它把 JMM 规范里"没有 happens-before 保证,读到的值是未定义的"这句听起来很学术的话,变成了两个真实、可复现、结果截然不同的程序行为——一个是 2000 次跨线程读取零差错,另一个是线程真实被永久卡死。这也是为什么系列前面 volatile 那篇反复强调的"不只是防止指令重排,更是提供可见性保证"这句话,在这里用最直接的方式被真实验证了。

分步演示:一条完整的 happens-before 链是怎么真实拼起来的

每一行都是真实运行这段代码得到的结果。

happens-before 不是一条规则,是一整套规则(程序顺序、volatile、监视器锁、线程start/join、类初始化)真实拼起来的可见性保证体系
这一步新验证的规则

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,volatile场景2000轮零错误、无volatile场景5轮全部真实死循环,两组结果重复运行完全一致(为避免JVM无法退出,reader线程被设为守护线程)。
    • 和volatile、this 逃逸两篇一起看,能拼出 Java 内存模型里"可见性"这条主线从理论到真实现象的完整证据链。
    • 没有涉及:synchronized块进入/退出构成的happens-before规则真实验证、Thread.start()/join()构成的happens-before规则真实验证——这两个话题留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号