You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

无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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 03:32:42