【多线程】---JMM 与 volatile 深度解析

基于:Java(内存模型规范 JSR-133,JDK 5+;示例基于 JDK 17 / HotSpot x86-64)

「一个规范(JMM)+ 一个关键字(volatile)」的线索讲透 Java 并发的可见性、原子性与有序性:JMM 是什么、它如何抽象硬件、volatile 的内存语义与内存屏障,最后用 3 个可运行 Demo 验证「volatile 能做什么、不能做什么」,并盘点 JDK 源码里的经典应用。


目录

  1. 核心理论
    • [1.1 JMM 是什么](#1.1 JMM 是什么)
    • [1.2 并发三要素定位表](#1.2 并发三要素定位表)
    • [1.3 JMM 与 JVM 运行时数据区的区别](#1.3 JMM 与 JVM 运行时数据区的区别)
  2. 设计思想与核心机制
    • [2.1 主内存与工作内存模型](#2.1 主内存与工作内存模型)
    • [2.2 内存间交互的 8 大原子操作](#2.2 内存间交互的 8 大原子操作)
    • [2.3 重排序与 as-if-serial](#2.3 重排序与 as-if-serial)
    • [2.4 happens-before 规则](#2.4 happens-before 规则)
    • [2.5 volatile 的内存语义与内存屏障](#2.5 volatile 的内存语义与内存屏障)
    • [2.6 volatile 与锁 / final / Atomic 的分工](#2.6 volatile 与锁 / final / Atomic 的分工)
  3. 主线流程剖析(示例驱动)
    • [3.1 volatile 写 / 读的完整流程](#3.1 volatile 写 / 读的完整流程)
    • [3.2 Demo 1:可见性------死循环能退出吗](#3.2 Demo 1:可见性——死循环能退出吗)
    • [3.3 Demo 2:原子性------volatile 修饰的 count++](#3.3 Demo 2:原子性——volatile 修饰的 count++)
    • [3.4 Demo 3:有序性------DCL 单例为什么必须 volatile](#3.4 Demo 3:有序性——DCL 单例为什么必须 volatile)
  4. [并发工具全景关系图 ★★★](#并发工具全景关系图 ★★★)
  5. 扩展点与常见问题
    • [5.1 JDK 源码中的 volatile 经典用法](#5.1 JDK 源码中的 volatile 经典用法)
    • [5.2 演进:VarHandle 与 LongAdder](#5.2 演进:VarHandle 与 LongAdder)
    • [5.3 FAQ](#5.3 FAQ)

1. 核心理论

1.1 JMM 是什么

JMM(Java Memory Model,Java 内存模型)是 JSR-133(JDK 5)确立的一套抽象规范 :它描述程序中对共享变量的读写行为在什么时候、以什么顺序对其它线程可见 ,以及什么情况下允许重排序。它不是硬件的真实内存布局,而是对「CPU 缓存、寄存器、写缓冲、乱序执行」等物理现象的统一抽象------屏蔽了 x86 / ARM / RISC-V 等平台的差异。

关键认知:JMM 只回答两个问题:

  1. 可见性:线程 A 写了一个变量,线程 B 什么时候一定看得到?
  2. 有序性:代码写在前面的操作,会不会被重排到后面去、并且被其它线程观察到?

规则只约束「共享变量」(堆中的实例字段、静态字段、数组元素),不约束线程私有数据(局部变量、方法参数)。

1.2 并发三要素定位表

并发 Bug 的根源收敛为三个问题,JMM 分别给出机制,语言级关键字/工具各自覆盖其中若干项:

问题 物理根源 JMM 的解决机制 语言级工具
可见性 CPU 缓存:线程各自读自己缓存里的旧副本 主内存-工作内存同步规则;volatile 的读/写屏障强制与主存交互 volatilesynchronizedLockfinal(构造后安全发布)
原子性 线程切换:多个指令之间被插入另一个线程的操作 仅对单条 read/load/use/assign 等单操作 保证原子;复合操作(如 i++)不保证 synchronized(互斥)、Atomic*(CAS)
有序性 编译器重排 + CPU 指令级乱序 + 内存系统重排 happens-before 规则;volatile 禁止特定方向的重排 volatilesynchronized

⭐ 一句话记忆:volatile 解决可见性和有序性,不解决原子性;synchronized 三者全包但更重;Atomic 只解决原子性(自带部分可见性语义)。

1.3 JMM 与 JVM 运行时数据区的区别

初学者最常见的混淆点。两者同名「内存」但完全不同维度的东西:

概念 关注问题 举例 属于
JVM 运行时数据区 对象、栈帧存哪里、谁来管理生命周期 堆、虚拟机栈、方法区、GC 具体的内存分配模型
JMM 共享变量在多线程下怎么读写才对、何时可见 volatile、happens-before、内存屏障 并发正确性规范

堆内存地址是「物理位置」,JMM 管的是「逻辑可见顺序」。可以类比:一个是仓库布局图,一个是货物流转的交接单据规则。


2. 设计思想与核心机制

2.1 主内存与工作内存模型

JMM 规定所有共享变量存在主内存 (Main Memory);每条线程有自己的工作内存 (Working Memory,保存用到的变量的副本)。线程对变量的所有读写都必须在工作内存中进行,不能直接读写主内存:

复制代码
┌─────────────── 线程 A ───────────────┐    ┌─────────────── 线程 B ───────────────┐
│  执行引擎 ◄──use──► 工作内存(副本)    │    │  执行引擎 ◄──use──► 工作内存(副本)    │
└───────────────┬─────────────────────┘    └───────────────┬─────────────────────┘
          load / store                              load / store
                └─────────────┬──────────┐ ┌──────────┬────────────┘
                              ▼          ▼ ▼          ▼
                     ┌──────────────────────────────────────┐
                     │            主内存(Main Memory)       │
                     └──────────────────────────────────────┘

对应到物理硬件,「工作内存」≈ CPU 寄存器 + 各级 Cache + 写缓冲,「主内存」≈ RAM。Java 用它做了一层与平台无关的抽象:
#mermaid-svg-NCs8lZjGoEC2eBgD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NCs8lZjGoEC2eBgD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NCs8lZjGoEC2eBgD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NCs8lZjGoEC2eBgD .error-icon{fill:#552222;}#mermaid-svg-NCs8lZjGoEC2eBgD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NCs8lZjGoEC2eBgD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NCs8lZjGoEC2eBgD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NCs8lZjGoEC2eBgD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NCs8lZjGoEC2eBgD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NCs8lZjGoEC2eBgD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NCs8lZjGoEC2eBgD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NCs8lZjGoEC2eBgD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NCs8lZjGoEC2eBgD .marker.cross{stroke:#333333;}#mermaid-svg-NCs8lZjGoEC2eBgD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NCs8lZjGoEC2eBgD p{margin:0;}#mermaid-svg-NCs8lZjGoEC2eBgD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NCs8lZjGoEC2eBgD .cluster-label text{fill:#333;}#mermaid-svg-NCs8lZjGoEC2eBgD .cluster-label span{color:#333;}#mermaid-svg-NCs8lZjGoEC2eBgD .cluster-label span p{background-color:transparent;}#mermaid-svg-NCs8lZjGoEC2eBgD .label text,#mermaid-svg-NCs8lZjGoEC2eBgD span{fill:#333;color:#333;}#mermaid-svg-NCs8lZjGoEC2eBgD .node rect,#mermaid-svg-NCs8lZjGoEC2eBgD .node circle,#mermaid-svg-NCs8lZjGoEC2eBgD .node ellipse,#mermaid-svg-NCs8lZjGoEC2eBgD .node polygon,#mermaid-svg-NCs8lZjGoEC2eBgD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NCs8lZjGoEC2eBgD .rough-node .label text,#mermaid-svg-NCs8lZjGoEC2eBgD .node .label text,#mermaid-svg-NCs8lZjGoEC2eBgD .image-shape .label,#mermaid-svg-NCs8lZjGoEC2eBgD .icon-shape .label{text-anchor:middle;}#mermaid-svg-NCs8lZjGoEC2eBgD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NCs8lZjGoEC2eBgD .rough-node .label,#mermaid-svg-NCs8lZjGoEC2eBgD .node .label,#mermaid-svg-NCs8lZjGoEC2eBgD .image-shape .label,#mermaid-svg-NCs8lZjGoEC2eBgD .icon-shape .label{text-align:center;}#mermaid-svg-NCs8lZjGoEC2eBgD .node.clickable{cursor:pointer;}#mermaid-svg-NCs8lZjGoEC2eBgD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NCs8lZjGoEC2eBgD .arrowheadPath{fill:#333333;}#mermaid-svg-NCs8lZjGoEC2eBgD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NCs8lZjGoEC2eBgD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NCs8lZjGoEC2eBgD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NCs8lZjGoEC2eBgD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NCs8lZjGoEC2eBgD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NCs8lZjGoEC2eBgD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NCs8lZjGoEC2eBgD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NCs8lZjGoEC2eBgD .cluster text{fill:#333;}#mermaid-svg-NCs8lZjGoEC2eBgD .cluster span{color:#333;}#mermaid-svg-NCs8lZjGoEC2eBgD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NCs8lZjGoEC2eBgD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NCs8lZjGoEC2eBgD rect.text{fill:none;stroke-width:0;}#mermaid-svg-NCs8lZjGoEC2eBgD .icon-shape,#mermaid-svg-NCs8lZjGoEC2eBgD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NCs8lZjGoEC2eBgD .icon-shape p,#mermaid-svg-NCs8lZjGoEC2eBgD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NCs8lZjGoEC2eBgD .icon-shape .label rect,#mermaid-svg-NCs8lZjGoEC2eBgD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NCs8lZjGoEC2eBgD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NCs8lZjGoEC2eBgD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NCs8lZjGoEC2eBgD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} CPU 物理视角
线程 A(工作内存)
use / assign
load / store
read / write

volatile 每次强制穿透
缓存一致性协议 MESI
执行引擎
变量副本

≈ 寄存器 + L1/L2 Cache
ALU
Cache

L1 / L2 / L3
主内存 Main Memory

≈ RAM

由此推出第一个结论 :普通变量在工作内存缓存 → 线程 A 改了,线程 B 可能一直读旧副本 ------ 这就是可见性问题的根源,也解释了为什么需要 volatile。

2.2 内存间交互的 8 大原子操作

JMM 定义了 8 种操作,分别覆盖「主内存 ↔ 工作内存 ↔ 执行引擎」三条边,每个操作原子执行:

操作 方向 作用
lock 主内存 把变量标识为某线程独占(锁的底层动作)
unlock 主内存 释放被独占的变量
read 主内存→工作内存 把变量值从主存读出(传输)
load 主内存→工作内存 把 read 到的值装入工作内存副本
use 工作内存→执行引擎 把工作内存副本值交给执行引擎(读取变量)
assign 执行引擎→工作内存 把执行引擎的计算结果赋给工作内存副本(写入变量)
store 工作内存→主内存 把副本值传出准备写回主存
write 工作内存→主内存 把 store 传出的值写入主内存变量

配套的执行规则(代表性条款):

  • ⚠️ readloadstorewrite 必须成对、按序出现,不允许单独丢弃
  • 不允许丢弃线程最近一次 assign 的操作;没有经过 assign 的变量不允许同步回主存(「写回前必须先有写入」)
  • 新变量必须诞生在主内存,不允许工作内存直接使用一个未初始化的变量
  • use 前必须先 load(除非该变量刚被本线程 assign);store 前必须先 assign
  • 同一时刻只允许一个线程 locklock清空 该线程工作内存中此变量的副本(必须重新 load);unlock 前必须先 store + write 同步回主存

关键认知 :第 5 条直接解释了 synchronized 为什么能保证可见性 ------进入同步块(lock)清空副本强制重新读取,退出同步块(unlock)强制写回主存。

2.3 重排序与 as-if-serial

为了性能,编译器与 CPU 会在「不改变单线程语义」的前提下打乱指令顺序,来源有三类:

重排序来源 说明 能不能通过 volatile 阻断
编译器优化重排 javac / JIT 认为顺序无关紧要时重排 ✅ 可以(屏障作用于 JIT 生成的代码)
指令级并行(乱序执行) CPU 流水线让无依赖指令并行 ✅ 可以(屏障指令限制)
内存系统重排 读缓冲 / 写缓冲使「提交到内存」的顺序变 ✅ 可以(缓存一致性+屏障)

as-if-serial 原则 :不管怎么重排,单线程 执行结果不变。真正的危险只发生在------一个线程的「乱序结果」被另一个线程观察到。所以 JMM 不管单线程内部,只管跨线程的可见顺序

2.4 happens-before 规则

happens-before(先行发生)是 JMM 判定「两个操作跨线程能否安全观察」的核心工具 :若 A happens-before B,则 A 对内存的写入对 B 可见,且 A 不会被重排到 B 之后。八大规则:

# 规则 内容
1 程序顺序规则 一个线程内,写在前的操作 HB 写在后的操作(as-if-serial)
2 监视器锁规则 同一个锁的 unlock HB 其后对该锁的 lock
3 volatile 变量规则 对一个 volatile 变量的 HB 其后对同一变量的读
4 线程启动规则 Thread.start() HB 被启动线程内的任何动作
5 线程终止规则 线程内所有动作 HB 其它线程通过 join() 返回后(或 isAlive() == false)的观察
6 线程中断规则 interrupt() 调用 HB 被中断线程检测到中断(InterruptedException / interrupted()
7 终结器规则 对象构造完成 HB 其 finalize() 的开始
8 传递性 A HB B 且 B HB C ⇒ A HB C

另有一条 final 域特殊规则 :在构造器内对 final 字段的写入,与随后将该对象引用发布(this 逸出除外)之间,JMM 保证不会重排------正确构造 + 正确发布后,final 字段无需 volatile 也全局可见(不可变对象安全的根本原因)。

2.5 volatile 的内存语义与内存屏障

volatile 是 JMM 里最轻量、最常用的同步手段,其语义可精确概括为两组规则

① 读写内存语义

  • volatile 写 :JMM 保证写入结果立即对其他线程可见 ------写操作会「穿透」工作内存缓存层,同步到主内存;同时使其它线程持有的该变量副本失效(底层依赖 MESI 等缓存一致性协议)。
  • volatile 读 :每次读取都从主内存重新获取,不使用可能失效的本地缓存。

② 禁止重排序的 4 条规则(JSR-133 精确定义)

场景 是否允许重排 含义
普通读/写 → volatile 写 volatile 写之前的操作必须已经发生
volatile 读 → 普通读/写 volatile 读之后的操作不得被提前观察到
volatile 写 → volatile 读 读写之间必须建立完整顺序
普通读/写 ↔ 普通读/写 与 volatile 无关处自由优化

内存屏障落地 (HotSpot JVM 视角):编译器在 volatile 读写位置插入内存屏障指令,禁止屏障两侧的指令穿越:

复制代码
volatile 写:  [普通操作] → StoreStore → StoreLoad → [普通操作]
                         └ 前面普通写先刷出    └ 阻止与后面的 volatile 读/写乱序
volatile 读:  [普通操作] → LoadLoad → LoadStore → [普通操作]
                         └ 后面的读/写不得提前越过此读

✅ x86-TSO 架构(我们常见的 Intel/AMD)下,处理器本身已保证除「写后读」(StoreLoad)之外的所有顺序,因此 HotSpot 在 x86 上:

  • volatile 读 ≈ 普通读,几乎零成本;
  • volatile 写 = 普通写 + 一条 lock addl(或 mfence)------只在写后插入 StoreLoad。
    而 ARM 等弱内存模型架构需要更完整的屏障组合,这也解释了为什么不同平台 JIT 生成的屏障代码不同。

2.6 volatile 与锁 / final / Atomic 的分工

维度 volatile synchronized / Lock final Atomic* / CAS
可见性 ✅(读写都穿透主存) ✅(lock 清缓存 / unlock 刷回) ✅(构造后安全发布) ✅(CAS 前读后写)
原子性(复合操作) ✅(互斥) --- ✅(CAS 自旋)
有序性 ✅(禁重排) ✅(临界区串行化) ✅(final 写规则) ✅(CAS 有全屏障)
阻塞 ❌ 不阻塞,无锁竞争 ✅ 会(可优化为偏向/轻量级) --- ❌ 自旋不阻塞
开销 最轻 重(上下文切换/竞争) 几乎为零 轻(竞争激烈时自旋浪费 CPU)
典型场景 状态标志、DCL、发布「只写一次」的值 复合操作临界区、互斥资源 不可变对象字段 计数器、并发容器内部

一句话概括:volatile 是「只同步、不互斥」的最轻量武器;需要复合操作原子性时必须上锁或 CAS;final 则免费送你「安全发布」。


3. 主线流程剖析(示例驱动)

3.1 volatile 写 / 读的完整流程

线程 B(读线程) 工作内存 B 主内存(volatile flag) 工作内存 A 线程 A(写线程) 线程 B(读线程) 工作内存 B 主内存(volatile flag) 工作内存 A 线程 A(写线程) #mermaid-svg-9puMy1HkphneWYY3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9puMy1HkphneWYY3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9puMy1HkphneWYY3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9puMy1HkphneWYY3 .error-icon{fill:#552222;}#mermaid-svg-9puMy1HkphneWYY3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9puMy1HkphneWYY3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9puMy1HkphneWYY3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9puMy1HkphneWYY3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9puMy1HkphneWYY3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9puMy1HkphneWYY3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9puMy1HkphneWYY3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9puMy1HkphneWYY3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9puMy1HkphneWYY3 .marker.cross{stroke:#333333;}#mermaid-svg-9puMy1HkphneWYY3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9puMy1HkphneWYY3 p{margin:0;}#mermaid-svg-9puMy1HkphneWYY3 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9puMy1HkphneWYY3 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-9puMy1HkphneWYY3 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-9puMy1HkphneWYY3 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-9puMy1HkphneWYY3 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-9puMy1HkphneWYY3 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-9puMy1HkphneWYY3 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-9puMy1HkphneWYY3 .sequenceNumber{fill:white;}#mermaid-svg-9puMy1HkphneWYY3 #sequencenumber{fill:#333;}#mermaid-svg-9puMy1HkphneWYY3 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-9puMy1HkphneWYY3 .messageText{fill:#333;stroke:none;}#mermaid-svg-9puMy1HkphneWYY3 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9puMy1HkphneWYY3 .labelText,#mermaid-svg-9puMy1HkphneWYY3 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-9puMy1HkphneWYY3 .loopText,#mermaid-svg-9puMy1HkphneWYY3 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-9puMy1HkphneWYY3 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-9puMy1HkphneWYY3 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-9puMy1HkphneWYY3 .noteText,#mermaid-svg-9puMy1HkphneWYY3 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-9puMy1HkphneWYY3 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9puMy1HkphneWYY3 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9puMy1HkphneWYY3 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9puMy1HkphneWYY3 .actorPopupMenu{position:absolute;}#mermaid-svg-9puMy1HkphneWYY3 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-9puMy1HkphneWYY3 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9puMy1HkphneWYY3 .actor-man circle,#mermaid-svg-9puMy1HkphneWYY3 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-9puMy1HkphneWYY3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 线程 B 再次读取时 发现副本失效 assign(引擎算出新值) store + write(volatile 写:立即刷主存,x86 加 lock/mfence) 广播失效该缓存行(缓存一致性协议) volatile 读:强制从主内存重新 load 读到最新值

关键点:volatile 写 → 其它线程的读 这条链,恰好命中 happens-before 规则 3,所以跨线程看到的顺序也是确定的(写前的普通写也一并可见------这就是「volatile 写前禁止重排」带来的写后同步效应)。

3.2 Demo 1:可见性------死循环能退出吗

java 复制代码
/**
 * 演示 volatile 的可见性语义
 * 运行预期:
 *  - 不加 volatile:worker 线程可能永远不退出(读自己缓存的旧值 + JIT 提升),main 卡在 join()
 *  - 加上 volatile:worker 1 秒后读到最新值,正常退出
 */
public class VolatileVisibilityDemo {

    static boolean stop = false;          // ① 先不加 volatile 运行观察
    // static volatile boolean stop = false;  // ② 改为 volatile 再运行

    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            long i = 0;
            while (!stop) {   // 非 volatile:可能被 JIT 优化为读取一次缓存副本 → 死循环
                i++;
            }
            System.out.println("worker stopped, iterations = " + i);
        });
        worker.start();

        Thread.sleep(1000);
        stop = true;                       // 主线程写入
        System.out.println("main set stop = true");
        worker.join();                     // 不加 volatile 时这里可能永远等不到
        System.out.println("main end");
    }
}

⚠️ 运行提示 :死循环是否复现依赖 JIT 与硬件,多跑几次或在 -server 下观察更稳定;volatile 版本则必然正常退出。

流程解释stop = true 发生在主线程 → worker 线程的读取之间。两者无锁、无其它 HB 关系,唯一能建立可见性的就是 volatile 的「写刷主存 + 读穿主存」。把 while 里的读取想成每次 volatile 读,就天然绕开了缓存陈旧问题。

3.3 Demo 2:原子性------volatile 修饰的 count++

java 复制代码
/**
 * 演示 volatile 不能保证复合操作的原子性
 * 运行预期:10 线程 × 1000 次自增,期望 10000;
 * 实际上 volatile 版本每次运行结果都 < 10000(且不稳定)
 */
public class VolatileNotAtomicDemo {

    static volatile int count = 0;        // volatile 只保证可见性,不保证复合原子性
    static final int THREADS = 10;
    static final int LOOP = 1000;

    public static void main(String[] args) throws InterruptedException {
        Thread[] ts = new Thread[THREADS];
        for (int i = 0; i < THREADS; i++) {
            ts[i] = new Thread(() -> {
                for (int j = 0; j < LOOP; j++) {
                    count++;              // 实际是 3 步:read → 自增 → write,中间可被切换
                }
            });
            ts[i].start();
        }
        for (Thread t : ts) {
            t.join();
        }
        System.out.println("count = " + count);   // volatile 版本:总是 < 10000
    }
}

修复方式(三选一)

java 复制代码
// 修复 1:synchronized 串行化整个 count++
synchronized void inc() { count++; }

// 修复 2:AtomicInteger 原子自增(推荐计数器场景)
static AtomicInteger count2 = new AtomicInteger(0);
// ... count2.incrementAndGet();  → 恒等于 10000

// 修复 3:LongAdder(高并发写多读少,热点分离,见 5.2)
static LongAdder count3 = new LongAdder();
// ... count3.increment(); → 恒等于 10000

流程解释count++ 在字节码层面是 GETSTATIC / IADD / PUTSTATIC 三段,volatile 只保证每一段自己原子,但三段之间 线程随时可能被切走:A 读到 5 → A 被切出 → B 读到 5、加到 6 写回 → A 回来拿着自己的 5 再加成 6 写回 → 丢了一次更新。可见性与原子性是正交的两件事,volatile 管不了后者。

3.4 Demo 3:有序性------DCL 单例为什么必须 volatile

java 复制代码
/**
 * 双重检查锁(Double-Checked Locking)单例
 * instance 必须 volatile!去掉后存在拿到「半初始化对象」的风险
 */
public class Singleton {

    private static volatile Singleton instance;   // 关键:禁止第②③步重排

    private Singleton() { /* 假设构造逻辑较重,如初始化资源 */ }

    public static Singleton getInstance() {
        if (instance == null) {                   // 1. 快速检查(无锁,绝大多数路径)
            synchronized (Singleton.class) {
                if (instance == null) {           // 2. 锁内复查(防止重复创建)
                    instance = new Singleton();   // 3. 真正创建
                }
            }
        }
        return instance;
    }
}

new 的三步拆解(JIT/CPU 可能把 ② 和 ③ 重排):

复制代码
① 在堆上分配内存
② 执行构造方法,完成初始化          ╮
③ 将引用赋值给 instance 字段        ╯ ②③ 无数据依赖 → 允许重排!

危险时序(无 volatile 时):
  A 线程:执行 ③(instance 已非 null,但对象还没构造完)
  B 线程:快速检查 instance != null → 直接 return → 使用半初始化对象(字段为默认值)→ 💥

volatile 的作用:volatile 写(③赋值)前禁止与任何操作重排 → ② 必然先于 ③ → 其它线程看到非 null 的 instance 时,构造一定已完整完成。

✅ 注意:把 ② 的副作用限制在构造器内(不在构造中把 this 发布出去、不写共享变量),volatile 就足够保证安全;JDK 9+ 的完整 DCL 通常仍保留 volatile(例如 Collections$SynchronizedMap 等大量 JDK 内部类就是这一写法)。


4. 并发工具全景关系图 ★★★

三要素 ↔ 工具手段 ↔ 底层机制的关系总览(面试画这张图就够):
#mermaid-svg-CdbzcVyuIchtVgs9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CdbzcVyuIchtVgs9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CdbzcVyuIchtVgs9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CdbzcVyuIchtVgs9 .error-icon{fill:#552222;}#mermaid-svg-CdbzcVyuIchtVgs9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CdbzcVyuIchtVgs9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CdbzcVyuIchtVgs9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CdbzcVyuIchtVgs9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CdbzcVyuIchtVgs9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CdbzcVyuIchtVgs9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CdbzcVyuIchtVgs9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CdbzcVyuIchtVgs9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CdbzcVyuIchtVgs9 .marker.cross{stroke:#333333;}#mermaid-svg-CdbzcVyuIchtVgs9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CdbzcVyuIchtVgs9 p{margin:0;}#mermaid-svg-CdbzcVyuIchtVgs9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CdbzcVyuIchtVgs9 .cluster-label text{fill:#333;}#mermaid-svg-CdbzcVyuIchtVgs9 .cluster-label span{color:#333;}#mermaid-svg-CdbzcVyuIchtVgs9 .cluster-label span p{background-color:transparent;}#mermaid-svg-CdbzcVyuIchtVgs9 .label text,#mermaid-svg-CdbzcVyuIchtVgs9 span{fill:#333;color:#333;}#mermaid-svg-CdbzcVyuIchtVgs9 .node rect,#mermaid-svg-CdbzcVyuIchtVgs9 .node circle,#mermaid-svg-CdbzcVyuIchtVgs9 .node ellipse,#mermaid-svg-CdbzcVyuIchtVgs9 .node polygon,#mermaid-svg-CdbzcVyuIchtVgs9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CdbzcVyuIchtVgs9 .rough-node .label text,#mermaid-svg-CdbzcVyuIchtVgs9 .node .label text,#mermaid-svg-CdbzcVyuIchtVgs9 .image-shape .label,#mermaid-svg-CdbzcVyuIchtVgs9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-CdbzcVyuIchtVgs9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CdbzcVyuIchtVgs9 .rough-node .label,#mermaid-svg-CdbzcVyuIchtVgs9 .node .label,#mermaid-svg-CdbzcVyuIchtVgs9 .image-shape .label,#mermaid-svg-CdbzcVyuIchtVgs9 .icon-shape .label{text-align:center;}#mermaid-svg-CdbzcVyuIchtVgs9 .node.clickable{cursor:pointer;}#mermaid-svg-CdbzcVyuIchtVgs9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CdbzcVyuIchtVgs9 .arrowheadPath{fill:#333333;}#mermaid-svg-CdbzcVyuIchtVgs9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CdbzcVyuIchtVgs9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CdbzcVyuIchtVgs9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CdbzcVyuIchtVgs9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CdbzcVyuIchtVgs9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CdbzcVyuIchtVgs9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CdbzcVyuIchtVgs9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CdbzcVyuIchtVgs9 .cluster text{fill:#333;}#mermaid-svg-CdbzcVyuIchtVgs9 .cluster span{color:#333;}#mermaid-svg-CdbzcVyuIchtVgs9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CdbzcVyuIchtVgs9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CdbzcVyuIchtVgs9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-CdbzcVyuIchtVgs9 .icon-shape,#mermaid-svg-CdbzcVyuIchtVgs9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CdbzcVyuIchtVgs9 .icon-shape p,#mermaid-svg-CdbzcVyuIchtVgs9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CdbzcVyuIchtVgs9 .icon-shape .label rect,#mermaid-svg-CdbzcVyuIchtVgs9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CdbzcVyuIchtVgs9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CdbzcVyuIchtVgs9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CdbzcVyuIchtVgs9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 写后读全链路顺序
unlock HB 后续 lock
CAS 自带全屏障
共享变量并发读写
可见性问题
原子性问题
有序性问题
volatile

读穿主存/写刷主存
synchronized / Lock

lock 清副本、unlock 刷回
final

安全发布
synchronized / Lock

互斥串行化
Atomic* / CAS

读-改-写 原子循环
volatile

禁重排(内存屏障)
synchronized / Lock

临界区内天然有序
happens-before 8 规则

可见性+有序性的判据

选型口诀:读多写少的状态开关用 volatile → 需要安全计数用 Atomic/LongAdder → 复合操作/临界区用锁 → 不可变对象把字段全标 final。


5. 扩展点与常见问题

5.1 JDK 源码中的 volatile 经典用法

类 / 组件 volatile 字段 用途
java.util.concurrent.locks.AbstractQueuedSynchronizer state 锁状态:unlock 为 volatile 写 → 后续获取线程通过 HB 看到临界区全部修改
ThreadPoolExecutor ctl(高 3 位 runState + 低 29 位 workerCount) 一个 int 装两件事,靠 CAS 原子修改,volatile 保证运行状态可见
FutureTask state(NEW→COMPLETING→...) 任务状态机,volatile 保证跨线程可见的推进
ConcurrentHashMap(JDK 8+) table(Node 数组)、Node.val/nextsizeCtl 无锁读靠 volatile 拿最新表引用与链表节点
StampedLock / ReentrantReadWriteLock(部分状态) 状态位 轻量状态可见
框架层:Disruptor Sequence(内部 volatile long + 缓存行填充) 无锁队列序号,写线程发布后消费线程立即可见
经典设计模式 DCL 单例的 instance 防重排(3.4 节)

⭐ 这些字段的共性:单写者多读者 + 不需要复合原子性,正好是 volatile 的甜点区。AQS 的 state 之所以敢用 volatile,是因为所有修改都发生在持锁/自旋 CAS 的受控路径上。

5.2 演进:VarHandle 与 LongAdder

  • VarHandle(JDK 9+)java.lang.invoke.VarHandle 提供比 volatile 更细粒度的内存访问语义(plain / opaque / acquire / release / volatile),还封装了 CAS、getAndAdd 等,并允许对齐、数组元素级操作。JDK 内部(如 ConcurrentHashMapForkJoinPool)已大规模用它替换裸 volatile 字段 + Unsafe:
java 复制代码
public class Point {
    private int x;
    private static final VarHandle XH;
    static {
        try {
            XH = MethodHandles.lookup().findVarHandle(Point.class, "x", int.class);
        } catch (ReflectiveOperationException e) {
            throw new ExceptionInInitializerError(e);
        }
    }
    public void setXRelaxed(int v) { XH.setRelease(this, v); }   // 比 volatile 更宽松
    public int getXAcquire()       { return (int) XH.getAcquire(this); }
    public boolean casX(int e, int n) { return XH.compareAndSet(this, e, n); }
}
  • LongAdder(JDK 8+) :高并发计数把单个 volatile base 拆成 Cell[] 热点分离,写线程只 CAS 自己那格,读时求和------解决「一个 volatile long 被所有线程抢 CAS」的伪共享与竞争,性能比 AtomicLong 高一个数量级(写多读少时)。

5.3 FAQ

Q1:volatile 到底能不能保证原子性?

单个 volatile 变量的单次读写是原子的 (包括 64 位 long/double,见 Q2),但 i++i = i + 1 这类复合操作不原子。回答面试时先说清这个区分。

Q2:long / double 读写原子吗?

JSR-133(JDK 5+)起规范强制要求所有实现对 long/double 的写原子 (旧规范允许 32 位 JVM 分两次写,已废除);再用 volatile 修饰则读、写都原子且可见。现代 HotSpot 上 64 位值读写本来就是一条指令,推荐共享的 long/double 一律标 volatile,消除一切疑虑。

Q3:volatile 会阻塞线程、有锁竞争吗?

不会。volatile 无锁、无阻塞、无上下文切换,只是内存屏障指令(x86 上写的成本约一条 lock 前缀指令,读几乎免费)。代价是牺牲了「缓存里囤着用」的优化空间------高频读共享值时会略慢于普通字段,但仍远快于加锁。

Q4:什么场景用 volatile,什么场景必须用 Atomic / 锁?

  • ✅ volatile:状态标志(stopshutdown)、DCL 单例、安全发布「只写一次/很少写」的值、AQS 式「写路径已被锁保护」的共享字段;
  • ❌ 必须锁 / CAS:计数器、累加、check-then-act(先判断再修改)、任何「读-改-写」复合操作。

Q5:volatile int[] arr 能保证数组元素可见吗?

不能。volatile 只作用于数组引用本身 (arr 指向哪),arr[i] = x 是普通写。需要数组元素级保证要用 AtomicIntegerArray 等,或保证元素写入发生在持有锁/volatile 发布点之前(借助传递性)。

Q6:volatile 修饰的对象内部字段可见吗?

volatile 引用与对象内部字段是两回事。但要害在于 happens-before 传递性:volatile 写之前的普通写(包括对被引用对象内部字段的写)都会随之发布------所以「先填好对象字段,再把它赋给 volatile 引用」是安全的发布模式(DCL 单例就是此模式的代表)。

Q7:JMM 规范说「volatile 读必须从主内存读」,HotSpot 实现真的每次都读主存吗?

从语义上讲「必须能看到最新写入」;实现层面靠缓存一致性协议(MESI)的缓存行失效完成------线程读时发现本地副本已失效就从缓存体系重新拉取(x86 上由总线窥探保证,未必真的触达 RAM)。规范管语义,实现管性能,两者不冲突。


回顾:JMM 是「跨平台并发正确性规范」,三要素是可见性/原子性/有序性;volatile 是其中最轻的武器------一个屏障符号(禁重排)+ 一条穿透规则(读写主存),解决可见性与有序性,不解决复合原子性。把它放进 4 章全景图的位置里理解,比死记面试题更有用。

相关推荐
邪修king1 小时前
Re:Linux系统篇(十九):进程篇(八): 进程等待详解:wait/waitpid,僵尸进程到底该如何回收
java·开发语言
啵啵啵鱼2 小时前
DS-顺序表
java·数据结构·笔记·后端·链表·obsidian
TinyMemory2 小时前
Java 面向对象核心入门(七):方法重载 Overload,同一个方法名能干不一样的活
java·面向对象·overload·方法重载
吴声子夜歌2 小时前
Java扩展——OkHttp
java·开发语言·okhttp
卓怡学长2 小时前
w210基于springboot反诈平台设计与实现
java·spring boot·spring·intellij-idea
萧瑟余晖2 小时前
Java深入解析篇六十一之编译器API(javax.tools)详解
java·开发语言
码云数智-园园3 小时前
C++ STL 容器怎么选?vector、list、map、unordered_map 优缺点对比
java·开发语言
en.en..3 小时前
竖线 |:管道、按位或、掩码组合
java·开发语言