1. 首页
  2. 面试专题
  3. 文章列表
多家公司 Java 后端 Java 内存模型 2026-06-14

volatile 解决不了原子性:Java 内存模型真正想考什么

volatile 的价值在可见性和有序性,不在复合操作的原子性。并发题要从共享状态、执行顺序和一致性边界讲起。

Java 并发面试里,volatile 是一个很好的分水岭。能说出“保证可见性、不保证原子性”的人很多,但真正被追问时,面试官往往会换成项目问题:为什么一个开关变量改了,另一个线程还看不到?为什么加了 volatile,计数还是不对?为什么 synchronized 既能互斥又能保证可见性?

这类题的核心不是背定义,而是理解多线程程序里有三件事必须分开看:共享状态能不能被及时看到,操作能不能被打断,执行顺序会不会被重排。

可见性不是实时同步

每个线程执行时可能使用寄存器、CPU 缓存和工作内存里的数据。如果一个线程修改了共享变量,另一个线程不一定立刻读取到最新值。volatile 写入会把新值发布出去,volatile 读取会从主内存语义上看到最新写入,这就是它常被用在停止标记、配置开关、状态发布里的原因。

但这不意味着 volatile 是实时消息系统。它解决的是变量读写的可见性和顺序约束,不负责让业务流程一定在某个时间点响应。比如线程在阻塞 IO、长循环内部很少检查标记,volatile 变量更新了也不会立刻停下来。

i++ 不是一次操作

计数器加一看起来是一行代码,实际至少包含读、加、写。volatile 可以保证读到较新的值,也可以保证写出去的值对其他线程可见,但不能阻止两个线程同时读到同一个旧值,再各自写回相同的新值。所以 volatile int 做并发计数,结果仍然可能丢失。

这里要引出原子类或锁。AtomicInteger 的自增依赖 CAS 循环,适合简单计数和轻量状态变更;synchronized 或 ReentrantLock 适合把一组操作包成临界区。选择哪一个,不是看哪个更“高级”,而是看你要保护的是一个变量,还是一段业务不变量。

happens-before 是表达顺序的语言

happens-before 可以理解为 Java 内存模型里描述“前一个操作的结果对后一个操作可见”的规则。线程启动、线程 join、锁释放与获取、volatile 写与读,都能建立这样的关系。

面试里不需要把所有规则逐字背完,但要能用它解释现象。比如一个线程在加锁代码块里修改对象,退出锁后另一个线程进入同一把锁,后者能看到前者的修改;再比如一个线程写 volatile 标记前完成了配置初始化,另一个线程读到标记后,也能看到标记前发布的那些写入结果。

真实项目里最怕“看似没共享”

很多并发 bug 不会出现在明显的计数器上,而是出现在缓存刷新、异步任务状态、批处理进度、单例初始化、定时任务和回调链路里。代码表面上只是把任务丢到线程池,实际已经把对象引用、上下文、状态字段交给另一个线程了。

排查这类问题时,可以问自己三个问题:这个状态是否被多个线程读写;是否有明确的同步边界;复合操作是否需要整体一致。如果答案含糊,就不要只靠 volatile 碰运气。

面试表达要落到边界

一个成熟回答可以这样收束:volatile 适合状态标记和安全发布,不能替代锁处理复合操作;Atomic 适合单变量原子更新,锁适合维护多个变量之间的不变量;线程池、回调和异步任务会放大共享状态风险,所以项目里要尽量减少可变共享对象,并用清晰的同步边界表达所有权。

这样回答,比单纯背“可见性、有序性、原子性”更接近真实后端开发。