volatile的可见性保证(JMM视角)+ 原子性缺失原因(从8个原子操作角度)
要讲清这个问题,首先明确Java内存模型(JMM)的核心基础:所有变量的真实存储是主内存,每个线程有独立的工作内存(CPU缓存/寄存器),线程对变量的所有操作必须在工作内存中执行,不能直接操作主内存;而线程间的变量数据交互,必须通过主内存完成——这个交互过程,JMM定义了8个不可分割的原子操作,这是所有内存操作的最小单位,volatile的所有特性都基于对这8个原子操作的约束/未约束实现。
先列出JMM规定的8个原子操作(后续全程围绕这8个操作分析,不可拆分):
- lock(锁定):作用于主内存变量,把变量标识为当前线程独占状态;
- unlock(解锁):作用于主内存变量,解除独占状态,解锁后变量才能被其他线程lock;
- read(读取):作用于主内存变量,把主内存的变量值传输到线程的工作内存,为后续load做准备;
- load(载入):作用于工作内存变量,把read从主内存获取的值放入工作内存的变量副本中;
- use(使用):作用于工作内存变量,把工作内存的变量值传递给线程的执行引擎(如计算、赋值);
- assign(赋值):作用于工作内存变量,把执行引擎的计算结果赋值给工作内存的变量副本;
- store(存储):作用于工作内存变量,把工作内存的变量值传输到主内存,为后续write做准备;
- write(写入):作用于主内存变量,把store从工作内存获取的值放入主内存的变量中。
关键前提:这8个操作本身都是原子的、不可中断的,JMM保证单个操作的原子性;但多个原子操作的组合,若无额外约束,是可中断的——这是volatile不能保证原子性的核心伏笔。
一、volatile如何基于8个原子操作保证可见性?
volatile对可见性的保证,本质是通过JMM的强制规则,约束了线程操作volatile变量时,8个原子操作的执行顺序、执行时机,杜绝了“工作内存与主内存数据不一致”的可能,核心是对写操作和读操作分别做了严格的原子操作执行约束,且底层通过内存屏障保证这些约束不会被JVM/CPU重排序优化。
1. volatile变量「写操作」的原子操作约束(线程修改变量时)
普通变量的写操作,执行assign(工作内存赋值)后,store+write(工作内存→主内存)的执行时机是不确定的(JVM会缓存优化,可能延迟执行),这是写操作可见性失效的根源;
而volatile变量的写操作,JMM强制要求:
线程对volatile变量执行assign(工作内存赋值)后,必须立即、连续执行store+write原子操作,将工作内存的新值同步到主内存,不允许缓存、不允许延迟。
简单说:volatile写 = assign → 强制紧跟 store → write(三步连续执行,无间隙)。
2. volatile变量「读操作」的原子操作约束(线程读取变量时)
普通变量的读操作,会优先执行use(使用工作内存旧副本),不会主动执行read+load(从主内存刷新),这是读操作可见性失效的根源;
而volatile变量的读操作,JMM强制要求:
线程对volatile变量执行use(使用变量)前,必须先执行read+load原子操作,从主内存读取最新值并刷新到工作内存,不允许使用工作内存的旧副本。
简单说:volatile读 = read → load → 强制前置 use(三步连续执行,无间隙)。
3. 可见性的完整原子操作流程(线程A修改,线程B读取)
假设volatile变量flag初始值为false,主内存存储真实值,线程A、B有各自工作内存副本:
- 线程A执行
flag = true:先在工作内存执行assign(将副本赋值为true)→ 强制执行store(工作内存→主内存传输)→write(主内存更新为true); - 线程B执行
if(flag):先强制执行read(主内存读取true)→load(工作内存副本刷新为true)→ 再执行use(将true传递给执行引擎); - 全程无缓存延迟、无旧副本使用,线程B必然获取线程A修改后的最新值,实现可见性。
核心:volatile通过写操作强制紧跟store+write、读操作强制前置read+load,约束了8个原子操作的执行逻辑,让工作内存与主内存的同步变成“即时性”,从根本上解决了可见性问题。
二、从8个原子操作角度,说明volatile为什么不能保证原子性
首先重申两个关键前提:
- volatile能保证单个8个原子操作的原子性(如单独的read、load、store、write等,本身就是原子的);
- 原子性的核心要求是“一组操作作为整体,不可分割、不可中断”,即这组操作的执行过程中,不能插入其他线程的任何操作。
而volatile无法保证“多个8个原子操作的组合”作为整体的原子性——这是原子性缺失的根本原因。所有volatile的非原子操作(如i++、i--、a=b+1),本质都是拆解为多个JMM原子操作的组合,且volatile对这些组合操作的原子操作执行间隙无任何同步约束,线程可以在间隙中切换,导致操作结果错误。
典型案例:volatile修饰的i++(最易理解的复合操作)
即使int i被volatile修饰,i++依然是非原子操作,因为它会被JVM强制拆解为4个连续的JMM原子操作(组合操作,不可合并),且这4个操作的执行间隙无任何锁/同步约束,可被其他线程中断:
i++的4个JMM原子操作拆解(固定顺序,不可拆分)
volatile i++ = ①read → ②load → ③use → ④assign → ⑤store → ⑥write
简化为核心执行逻辑(聚焦数据交互关键步骤):
- read+load:从主内存读取i的最新值(read),载入到工作内存副本(load);
- use+assign:将工作内存的i值传递给执行引擎做+1计算(use),把计算结果重新赋值给工作内存的i副本(assign);
- store+write:将工作内存的新值传输到主内存(store),写入并更新主内存的i值(write)。
关键:这6个原子操作是依次执行的,操作之间存在可中断的时间窗口——volatile仅保证了「assign后立即执行store+write」「use前先执行read+load」(可见性约束),但没有任何机制保证这6个操作作为一个整体“不可中断”。
原子性缺失的具体过程(基于8个原子操作,线程切换导致结果错误)
假设初始值:volatile int i = 0(主内存i=0,线程A、B工作内存副本均为0),线程A、B同时执行i++,预期结果2,实际大概率1,全程围绕8个原子操作的执行间隙分析:
- 线程A执行i++:先完成①read+②load(从主内存读0,载入工作内存),此时工作内存i=0;
- 线程切换(核心间隙):CPU从线程A切到线程B,线程A的③use+④assign+⑤store+⑥write未执行;
- 线程B完整执行i++的6个原子操作:read+load(读主内存0,载入工作内存)→ use+assign(+1得1,赋值给工作内存)→ store+write(刷回主内存,主内存i=1);
- 线程切回:CPU回到线程A,继续执行未完成的操作;
- 线程A执行③use+④assign:基于之前load的旧值0(而非主内存最新的1)做+1,得到1,赋值给工作内存;
- 线程A执行⑤store+⑥write:将1刷回主内存,最终主内存i=1(而非预期2)。
原子性缺失的核心本质(8个原子操作视角)
- 复合操作(如i++)是多个JMM原子操作的组合,这是客观事实,无法改变;
- volatile仅约束了单个原子操作的执行时机/顺序(保证可见性),但未对多个原子操作的组合施加任何“原子性约束”(如lock锁定、禁止线程切换);
- 多个原子操作的执行间隙存在线程切换的时间窗口,其他线程可在该窗口中完整执行同一复合操作的所有原子操作,导致当前线程基于“旧值”继续执行,最终结果错误。
简单说:volatile管得到单个原子操作,管不到多个原子操作的组合,而原子性要求的是“组合操作的整体不可中断”——这就是从8个原子操作角度,volatile不能保证原子性的根本原因。
三、核心总结(紧扣8个原子操作)
- 可见性的保证:volatile通过强制约束8个原子操作的执行顺序和时机实现——写操作
assign后立即执行store+write,读操作use前必须先执行read+load,让工作内存与主内存即时同步,无缓存延迟、无旧副本使用; - 原子性的缺失:JMM的8个原子操作本身不可分割,但复合操作会拆解为多个原子操作的组合,volatile仅保证单个原子操作的原子性,无法保证组合操作的整体原子性——组合操作的原子操作之间存在可中断的间隙,线程切换会导致基于旧值执行,最终结果错误;
- 关键区别:可见性是“数据同步问题”,通过约束原子操作的执行逻辑即可解决;原子性是“操作整体不可中断问题”,需要对多个原子操作的组合施加锁/同步约束(如lock/unlock),而volatile不具备该能力。
补充:如何让复合操作具备原子性(8个原子操作视角)
若要让volatile变量的复合操作具备原子性,本质是通过额外机制,让复合操作的多个原子操作组合“获得整体原子性”,核心方式是利用JMM的lock+unlock原子操作:
- synchronized/ReentrantLock:执行复合操作前先执行
lock(锁定主内存变量,独占),让组合操作的所有原子操作在独占状态下执行,执行完成后执行unlock(解锁)——锁定期间其他线程无法执行任何原子操作,保证组合操作不可中断; - Atomic原子类(如AtomicInteger):基于CAS(比较并交换) 机制,将复合操作的多个原子操作与“主内存值对比”绑定,若工作内存值与主内存值不一致(说明被其他线程修改),则放弃当前操作并重新执行,间接保证组合操作的原子性(无锁实现)。

浙公网安备 33010602011771号