为何该JVM编译器优化合法?——《Effective Java》并发代码疑问
我正在阅读Joshua Bloch所著的《Effective Java》一书,作者在并发章节展示了如下代码片段:
public class StopThread { private static boolean stopRequested; public static void main(String[] args) throws InterruptedException { Thread backgroundThread = new Thread(() -> { int i = 0; while (!stopRequested) { i++; } }); backgroundThread.start(); TimeUnit.SECONDS.sleep(1); stopRequested = true; } }
随后作者提到:
在没有同步的情况下,虚拟机完全可以将代码:
while (!stopRequested) i++;转换为:
if (!stopRequested) while (true) i++;
对此我感到震惊,这完全改变了代码的语义,二者并不等价,为何虚拟机可以进行这种优化(甚至称其为优化都不合理)?能否有人为我解释?
这是基于JVM的「单线程语义一致性」假设
JVM的优化器默认遵循「as-if-serial」规则:只要优化后的代码在单线程环境下的执行结果和原代码完全一致,就允许进行优化。在单线程中,stopRequested的值不会在循环过程中被修改(无同步时JVM不会考虑其他线程的操作),所以把循环条件的检查移到循环外,执行结果和原代码没有区别——单线程里要么一开始就不进循环,要么一直循环下去。核心原因是「可见性」未被保证
在多线程无同步的场景下,JVM没有义务保证一个线程对stopRequested的修改能被另一个线程立刻看到。后台线程可能会把stopRequested的值缓存到自己的工作内存(CPU寄存器或L1缓存)里,而非每次循环都去主内存读取。这种缓存优化能大幅提升性能——避免每次循环都做一次昂贵的主内存访问。这种优化是合理的(从JVM性能角度)
单线程程序是绝大多数程序的常态,JVM的优化策略优先服务于单线程性能。对于没有显式同步(比如synchronized、volatile)的代码,JVM默认认为不存在跨线程的内存可见性需求,所以会大胆做这类缓存和重排序优化。只有当你显式声明了同步需求(比如给stopRequested加上volatile修饰符),JVM才会禁止这种优化,保证每次读取都从主内存获取最新值。为什么看起来语义改变了?
你觉得语义改变是站在多线程视角,但JVM的优化是基于单线程语义合法的前提。在没有同步的多线程场景下,Java内存模型(JMM)本身就不保证线程间的内存可见性,原代码的「后台线程能看到主线程修改的stopRequested」这个行为其实是没有保障的——JVM允许后台线程永远看不到这个修改,所以把循环改成「一次检查后无限循环」,只是把这种未定义行为明确表现出来了而已。
内容的提问来源于stack exchange,提问作者morpheus

