基于:Java(内存模型规范 JSR-133,JDK 5+;示例基于 JDK 17 / HotSpot x86-64)
「一个规范(JMM)+ 一个关键字(volatile)」的线索讲透 Java 并发的可见性、原子性与有序性:JMM 是什么、它如何抽象硬件、volatile 的内存语义与内存屏障,最后用 3 个可运行 Demo 验证「volatile 能做什么、不能做什么」,并盘点 JDK 源码里的经典应用。
目录
- 核心理论
- [1.1 JMM 是什么](#1.1 JMM 是什么)
- [1.2 并发三要素定位表](#1.2 并发三要素定位表)
- [1.3 JMM 与 JVM 运行时数据区的区别](#1.3 JMM 与 JVM 运行时数据区的区别)
- 设计思想与核心机制
- [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.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)
- [并发工具全景关系图 ★★★](#并发工具全景关系图 ★★★)
- 扩展点与常见问题
- [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 只回答两个问题:
- 可见性:线程 A 写了一个变量,线程 B 什么时候一定看得到?
- 有序性:代码写在前面的操作,会不会被重排到后面去、并且被其它线程观察到?
规则只约束「共享变量」(堆中的实例字段、静态字段、数组元素),不约束线程私有数据(局部变量、方法参数)。
1.2 并发三要素定位表
并发 Bug 的根源收敛为三个问题,JMM 分别给出机制,语言级关键字/工具各自覆盖其中若干项:
| 问题 | 物理根源 | JMM 的解决机制 | 语言级工具 |
|---|---|---|---|
| 可见性 | CPU 缓存:线程各自读自己缓存里的旧副本 | 主内存-工作内存同步规则;volatile 的读/写屏障强制与主存交互 | volatile、synchronized、Lock、final(构造后安全发布) |
| 原子性 | 线程切换:多个指令之间被插入另一个线程的操作 | 仅对单条 read/load/use/assign 等单操作 保证原子;复合操作(如 i++)不保证 |
synchronized(互斥)、Atomic*(CAS) |
| 有序性 | 编译器重排 + CPU 指令级乱序 + 内存系统重排 | happens-before 规则;volatile 禁止特定方向的重排 | volatile、synchronized |
⭐ 一句话记忆: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 传出的值写入主内存变量 |
配套的执行规则(代表性条款):
- ⚠️
read与load、store与write必须成对、按序出现,不允许单独丢弃 - 不允许丢弃线程最近一次
assign的操作;没有经过assign的变量不允许同步回主存(「写回前必须先有写入」) - 新变量必须诞生在主内存,不允许工作内存直接使用一个未初始化的变量
use前必须先load(除非该变量刚被本线程assign);store前必须先assign- 同一时刻只允许一个线程
lock;lock会清空 该线程工作内存中此变量的副本(必须重新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/next、sizeCtl |
无锁读靠 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 内部(如ConcurrentHashMap、ForkJoinPool)已大规模用它替换裸 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:状态标志(
stop、shutdown)、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 章全景图的位置里理解,比死记面试题更有用。