volatile的可见性保证(JMM视角)+ 原子性缺失原因(从8个原子操作角度)

要讲清这个问题,首先明确Java内存模型(JMM)的核心基础:所有变量的真实存储是主内存,每个线程有独立的工作内存(CPU缓存/寄存器),线程对变量的所有操作必须在工作内存中执行,不能直接操作主内存;而线程间的变量数据交互,必须通过主内存完成——这个交互过程,JMM定义了8个不可分割的原子操作,这是所有内存操作的最小单位,volatile的所有特性都基于对这8个原子操作的约束/未约束实现

先列出JMM规定的8个原子操作(后续全程围绕这8个操作分析,不可拆分):

  1. lock(锁定):作用于主内存变量,把变量标识为当前线程独占状态;
  2. unlock(解锁):作用于主内存变量,解除独占状态,解锁后变量才能被其他线程lock;
  3. read(读取):作用于主内存变量,把主内存的变量值传输到线程的工作内存,为后续load做准备;
  4. load(载入):作用于工作内存变量,把read从主内存获取的值放入工作内存的变量副本中;
  5. use(使用):作用于工作内存变量,把工作内存的变量值传递给线程的执行引擎(如计算、赋值);
  6. assign(赋值):作用于工作内存变量,把执行引擎的计算结果赋值给工作内存的变量副本;
  7. store(存储):作用于工作内存变量,把工作内存的变量值传输到主内存,为后续write做准备;
  8. 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强制紧跟 storewrite(三步连续执行,无间隙)。

2. volatile变量「读操作」的原子操作约束(线程读取变量时)

普通变量的读操作,会优先执行use(使用工作内存旧副本),不会主动执行read+load(从主内存刷新),这是读操作可见性失效的根源;
volatile变量的读操作,JMM强制要求
线程对volatile变量执行use(使用变量)前,必须先执行read+load原子操作,从主内存读取最新值并刷新到工作内存,不允许使用工作内存的旧副本
简单说:volatile读 = readload强制前置 use(三步连续执行,无间隙)。

3. 可见性的完整原子操作流程(线程A修改,线程B读取)

假设volatile变量flag初始值为false,主内存存储真实值,线程A、B有各自工作内存副本:

  1. 线程A执行flag = true:先在工作内存执行assign(将副本赋值为true)→ 强制执行store(工作内存→主内存传输)→ write(主内存更新为true);
  2. 线程B执行if(flag):先强制执行read(主内存读取true)→ load(工作内存副本刷新为true)→ 再执行use(将true传递给执行引擎);
  3. 全程无缓存延迟、无旧副本使用,线程B必然获取线程A修改后的最新值,实现可见性。

核心:volatile通过写操作强制紧跟store+write、读操作强制前置read+load,约束了8个原子操作的执行逻辑,让工作内存与主内存的同步变成“即时性”,从根本上解决了可见性问题。

二、从8个原子操作角度,说明volatile为什么不能保证原子性

首先重申两个关键前提:

  1. volatile能保证单个8个原子操作的原子性(如单独的read、load、store、write等,本身就是原子的);
  2. 原子性的核心要求是“一组操作作为整体,不可分割、不可中断”,即这组操作的执行过程中,不能插入其他线程的任何操作。

而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

简化为核心执行逻辑(聚焦数据交互关键步骤):

  1. read+load:从主内存读取i的最新值(read),载入到工作内存副本(load);
  2. use+assign:将工作内存的i值传递给执行引擎做+1计算(use),把计算结果重新赋值给工作内存的i副本(assign);
  3. 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个原子操作的执行间隙分析

  1. 线程A执行i++:先完成①read+②load(从主内存读0,载入工作内存),此时工作内存i=0;
  2. 线程切换(核心间隙):CPU从线程A切到线程B,线程A的③use+④assign+⑤store+⑥write未执行;
  3. 线程B完整执行i++的6个原子操作:read+load(读主内存0,载入工作内存)→ use+assign(+1得1,赋值给工作内存)→ store+write(刷回主内存,主内存i=1);
  4. 线程切回:CPU回到线程A,继续执行未完成的操作;
  5. 线程A执行③use+④assign:基于之前load的旧值0(而非主内存最新的1)做+1,得到1,赋值给工作内存;
  6. 线程A执行⑤store+⑥write:将1刷回主内存,最终主内存i=1(而非预期2)。

原子性缺失的核心本质(8个原子操作视角)

  1. 复合操作(如i++)是多个JMM原子操作的组合,这是客观事实,无法改变;
  2. volatile仅约束了单个原子操作的执行时机/顺序(保证可见性),但未对多个原子操作的组合施加任何“原子性约束”(如lock锁定、禁止线程切换);
  3. 多个原子操作的执行间隙存在线程切换的时间窗口,其他线程可在该窗口中完整执行同一复合操作的所有原子操作,导致当前线程基于“旧值”继续执行,最终结果错误。

简单说:volatile管得到单个原子操作,管不到多个原子操作的组合,而原子性要求的是“组合操作的整体不可中断”——这就是从8个原子操作角度,volatile不能保证原子性的根本原因。

三、核心总结(紧扣8个原子操作)

  1. 可见性的保证:volatile通过强制约束8个原子操作的执行顺序和时机实现——写操作assign后立即执行store+write,读操作use前必须先执行read+load,让工作内存与主内存即时同步,无缓存延迟、无旧副本使用;
  2. 原子性的缺失:JMM的8个原子操作本身不可分割,但复合操作会拆解为多个原子操作的组合,volatile仅保证单个原子操作的原子性,无法保证组合操作的整体原子性——组合操作的原子操作之间存在可中断的间隙,线程切换会导致基于旧值执行,最终结果错误;
  3. 关键区别:可见性是“数据同步问题”,通过约束原子操作的执行逻辑即可解决;原子性是“操作整体不可中断问题”,需要对多个原子操作的组合施加锁/同步约束(如lock/unlock),而volatile不具备该能力。

补充:如何让复合操作具备原子性(8个原子操作视角)

若要让volatile变量的复合操作具备原子性,本质是通过额外机制,让复合操作的多个原子操作组合“获得整体原子性”,核心方式是利用JMM的lock+unlock原子操作:

  1. synchronized/ReentrantLock:执行复合操作前先执行lock(锁定主内存变量,独占),让组合操作的所有原子操作在独占状态下执行,执行完成后执行unlock(解锁)——锁定期间其他线程无法执行任何原子操作,保证组合操作不可中断;
  2. Atomic原子类(如AtomicInteger):基于CAS(比较并交换) 机制,将复合操作的多个原子操作与“主内存值对比”绑定,若工作内存值与主内存值不一致(说明被其他线程修改),则放弃当前操作并重新执行,间接保证组合操作的原子性(无锁实现)。
posted @ 2026-01-30 10:30  先弓  阅读(1)  评论(0)    收藏  举报