无synchronized/volatile时线程内存可见性:线程修改是否永不可见?
关于无同步时线程修改可见性的问题解答
嘿,你的这个问题问到了Java并发模型的核心点上——首先得纠正一个误区:你提到的「不使用synchronized或volatile关键字时,一个线程做出的修改永远不会被另一个线程看到(或结果不确定)」这个说法不准确:不是「永远看不到」,而是没有任何机制保证一定能看到,最终结果完全是不确定的,这正是你看到两种不同运行结果的原因。
你的程序行为拆解
先把你的代码整理成更易读的格式:
public class VolatileTest implements Runnable { private int i = 0; @Override public void run() { i++; i++; } public int get() { return i; } public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); VolatileTest volatileTest = new VolatileTest(); executorService.execute(volatileTest); while (true) { int i = volatileTest.get(); if (i % 2 != 0) { System.exit(0); } } } }
你观察到的两种结果,其实都完全符合Java内存模型(JMM)的规则:
- 程序永远不终止:这是最容易被预期的场景——main线程一直在自己的工作内存里读取
i的初始值0,而执行线程对i的两次自修改(0→1→2)没有被同步到主内存,或者main线程压根没去主内存刷新最新值,于是陷入死循环。 - 程序意外退出(检测到i为奇数):这是因为执行线程的修改没有被原子性地同步到主内存。
i++本身是拆分为「读值→加1→写回」的三步操作,main线程可能恰好读取到了第一次自增后的中间值(i=1),而没等到第二次自增完成后的最终值(i=2)。这种情况可能是因为CPU缓存的偶然刷新、JVM没有做激进的优化,导致main线程「碰巧」看到了中间状态。
背后的核心原理
Java内存模型里,每个线程都有自己的「工作内存」,对变量的读写默认是先操作工作内存里的副本,再同步到主内存(这个同步时机没有强制要求):
- 可见性缺失:没有volatile或synchronized的约束时,JVM、编译器、CPU都可能做优化——比如线程修改变量后不立刻刷回主内存,或者其他线程一直读自己的工作内存副本,这就导致了「可能看到、可能看不到」的不确定状态。
- 原子性缺失:
i++不是原子操作,即使只有一个执行线程,两次自增的中间状态也可能被其他线程读取到;如果是多个执行线程,还会出现多个线程同时修改导致的竞态条件。
如何修复这个程序
如果想让程序的行为变得可预测,可以做以下任一修改:
- 把
i声明为volatile:private volatile int i = 0;,这会强制线程每次读取i都从主内存获取,修改后立刻刷回主内存,但注意volatile不保证i++的原子性,如果是多个执行线程,还是会出现问题; - 用
synchronized修饰run和get方法,保证每次读写i都在同一个锁的保护下,同时保证可见性和原子性; - 使用
AtomicInteger代替int,它提供了原子性的自增操作(incrementAndGet())和天然的可见性保证,是处理这类简单计数场景的最佳实践。
内容的提问来源于stack exchange,提问作者Hel
相关产品推荐
相关产品推荐

