Java数据竞争疑问:无同步布尔变量为何可能引发无限循环?
问题背景
DZone上有这样的论断:当两个或更多线程在无同步的情况下访问同一个非final变量时会发生数据竞争。不使用同步可能导致修改对其他线程不可见,进而读取到过期数据,引发无限循环、数据结构损坏或计算错误等问题。文中给出如下代码,称其可能因读取线程永远无法感知写入线程的修改而陷入无限循环:
class Waiter implements Runnable { private boolean shouldFinish; void finish() { shouldFinish = true; } public void run() { long iteration = 0; while (!shouldFinish) { iteration++; } System.out.println("Finished after: " + iteration); } } class DataRace { public static void main(String[] args) throws InterruptedException { Waiter waiter = new Waiter(); Thread waiterThread = new Thread(waiter); waiterThread.start(); waiter.finish(); waiterThread.join(); } }
疑问点
- Java中boolean类型的读写属于原子操作,为啥还会出现无限循环?
- 即便某次循环没感知到修改,下一次循环总该能看到变化吧?
- 实际情况真的会无限循环吗?原因是读取线程永远不让渡CPU给写入线程吗?如果是这样,这还能被称为数据竞争吗?
解答
这段代码确实可能陷入无限循环,核心原因不是线程不让渡CPU,而是Java内存模型(JMM)的可见性规则和JIT编译器的优化,具体如下:
原子性≠可见性
boolean的读写是原子操作没错,但原子性仅保证操作不会被拆分执行,不保证修改后的数值能立刻同步到主存并被其他线程看到。每个线程都有自己的工作内存,会将主存中的变量拷贝到本地缓存使用。写入线程调用finish()修改shouldFinish后,这个修改可能仅停留在自身的工作内存中,没有同步到主存;而读取线程则一直从自己的工作内存中读取旧值,永远看不到更新。JIT编译器的激进优化
JIT编译器会对循环代码做优化,比如将while (!shouldFinish)中的变量读取直接缓存到寄存器中,不再每次循环都去主存重新读取。也就是说,代码会被优化成类似“如果第一次读取到shouldFinish为false,就直接进入无限循环,再也不检查主存中变量的最新值”,这直接导致读取线程永远感知不到写入线程的修改。这确实属于数据竞争
数据竞争的定义是:多个线程访问同一非final变量,至少有一个线程在执行写操作,且没有使用同步机制(如synchronized、volatile等)来协调这些访问。这里写入线程修改shouldFinish,读取线程循环读取该变量,完全符合数据竞争的条件,和线程是否让渡CPU无关——哪怕写入线程已经执行完finish()方法,读取线程仍可能因可见性问题看不到修改。
内容的提问来源于stack exchange,提问作者xpt

