引言:volatile能解决什么问题?
看这个经典的"停止线程"问题:
public class StopThread {
private boolean stop = false; // 没有volatile
public void run() {
new Thread(() -> {
while (!stop) {
// 执行任务
doWork();
}
System.out.println("线程停止");
}).start();
// 1秒后停止线程
Thread.sleep(1000);
stop = true;
System.out.println("已设置stop=true");
}
}
运行结果:
已设置stop=true
(线程永远不会停止!)
加上volatile后:
private volatile boolean stop = false; // 加volatile
运行结果:
已设置stop=true
线程停止 ← 正常停止了
三个问题:
- 为什么没有
volatile时线程看不到stop = true? volatile做了什么让线程能看到了?volatile是万能的吗?什么时候不能用?
本篇文章将深入volatile的底层原理,彻底理解这个轻量级同步机制。
一、volatile解决的两大问题
1.1 可见性问题
问题根源:CPU缓存
CPU 0 CPU 1
↓ ↓
L1 Cache (stop=false) L1 Cache (stop=false)
↓ ↓
主内存 (stop=false)
时刻1: CPU 0修改stop=true
CPU 0: L1 Cache (stop=true) ← 只在CPU 0的缓存中
CPU 1: L1 Cache (stop=false) ← CPU 1看不到!
volatile的解决方案:
private volatile boolean stop = false;
// 写操作
stop = true;
// → 立即刷新到主内存
// → 通知其他CPU缓存失效(MESI协议)
// 读操作
boolean value = stop;
// → 检测缓存行状态
// → 如果失效,从主内存重新加载
1.2 有序性问题
问题根源:指令重排序
// 没有volatile
int a = 1; // 1
int b = 2; // 2
boolean ready = true; // 3
// 可能重排序为:
ready = true; // 3
a = 1; // 1
b = 2; // 2
导致的问题:
// 线程1
a = 1;
b = 2;
ready = true; // 可能重排序到a、b之前
// 线程2
if (ready) {
assert a == 1; // 可能失败!
assert b == 2; // 可能失败!
}
volatile的解决方案:
int a = 1;
int b = 2;
volatile boolean ready = true; // volatile写
// → 插入内存屏障
// → 禁止前面的写重排序到后面
二、volatile的底层实现:内存屏障
2.1 内存屏障的概念
内存屏障(Memory Barrier/Fence):CPU指令,用于控制内存访问顺序
4种内存屏障:
| 屏障类型 | 作用 | 说明 |
|---|---|---|
| LoadLoad | Load1; LoadLoad; Load2 | Load1的数据装载先于Load2 |
| StoreStore | Store1; StoreStore; Store2 | Store1的数据对其他处理器可见先于Store2 |
| LoadStore | Load1; LoadStore; Store2 | Load1的数据装载先于Store2 |
| StoreLoad | Store1; StoreLoad; Load2 | Store1的数据对所有处理器可见先于Load2 |
开销:
- LoadLoad、StoreStore、LoadStore:较轻量
- StoreLoad:最重量级(相当于一个完整的内存屏障)
2.2 volatile写插入的屏障
普通写1
普通写2
← StoreStore屏障(禁止前面的普通写与volatile写重排序)
volatile写
← StoreLoad屏障(禁止volatile写与后面的读写重排序)
普通读1
普通写3
示例:
int a = 0;
int b = 0;
volatile int c = 0;
public void writer() {
a = 1; // 普通写
b = 2; // 普通写
← StoreStore屏障
c = 3; // volatile写
← StoreLoad屏障
int x = a; // 普通读(不会重排序到c=3之前)
}
保证的顺序:
a = 1、b = 2 一定在 c = 3 之前执行
c = 3 一定在 int x = a 之前执行
2.3 volatile读插入的屏障
普通读1
普通写1
← LoadLoad屏障(禁止后面的读重排序到volatile读之前)
← LoadStore屏障(禁止后面的写重排序到volatile读之前)
volatile读
普通读2
普通写2
示例:
volatile int c = 0;
int a = 0;
int b = 0;
public void reader() {
int x = a; // 普通读(可能重排序到c之前)
← LoadLoad屏障
← LoadStore屏障
int y = c; // volatile读
a = 1; // 普通写(不会重排序到c之前)
b = 2; // 普通写(不会重排序到c之前)
}
保证的顺序:
int y = c 一定在 a = 1、b = 2 之前执行
2.4 完整的volatile语义
综合示例:
public class VolatileSemantics {
private int x = 0;
private int y = 0;
private volatile int z = 0;
// 线程1
public void writer() {
x = 1; // 1
y = 2; // 2
← StoreStore屏障
z = 3; // 3 (volatile写)
← StoreLoad屏障
int a = x; // 4
}
// 线程2
public void reader() {
int b = z; // 5 (volatile读)
← LoadLoad屏障
← LoadStore屏障
int c = y; // 6
int d = x; // 7
}
}
happens-before关系:
1 hb 2 (程序顺序规则)
2 hb 3 (StoreStore屏障)
3 hb 5 (volatile规则)
5 hb 6 (LoadLoad屏障)
6 hb 7 (程序顺序规则)
传递性:
1 hb 7 (线程2能看到线程1的所有写入)
三、volatile的汇编实现
3.1 x86平台的实现
public class VolatileExample {
private volatile long v = 0L;
public void set(long value) {
v = value;
}
}
生成的汇编代码(简化):
# 普通long写入(无volatile)
movq $0x123, 0x10(%rax) # 直接写入内存
# volatile long写入
movq $0x123, %rdx # 值放入寄存器
lock addl $0x0, (%rsp) # lock前缀(内存屏障)
movq %rdx, 0x10(%rax) # 写入内存
lock前缀的作用:
- 锁定缓存行:当前CPU独占该缓存行
- 刷新到主内存:立即写回
- 失效其他缓存:通知其他CPU缓存失效(MESI协议)
- 内存屏障:禁止指令重排序
3.2 ARM平台的实现
# volatile读
dmb ish # 数据内存屏障(Data Memory Barrier)
ldr r0, [r1] # Load
# volatile写
str r0, [r1] # Store
dmb ish # 数据内存屏障
ARM的内存屏障指令:
- DMB(Data Memory Barrier):数据内存屏障
- DSB(Data Synchronization Barrier):数据同步屏障(更强)
- ISB(Instruction Synchronization Barrier):指令同步屏障
3.3 性能对比
| 平台 | 普通写入 | volatile写入 | 性能差距 |
|---|---|---|---|
| x86 | 1 cycle | 5-10 cycles | 5-10倍 |
| ARM | 1 cycle | 50-100 cycles | 50-100倍 |
结论:
- x86的TSO模型较强,volatile开销小
- ARM的弱内存模型需要显式屏障,开销大
- 但相比synchronized,volatile仍然轻量得多
四、volatile的典型使用场景
场景1:状态标志
最常见的用法:
public class Server {
private volatile boolean running = true;
public void run() {
while (running) { // 读volatile
handleRequest();
}
}
public void shutdown() {
running = false; // 写volatile
}
}
为什么合适:
- 只有简单的读写操作
- 不需要原子性保证
- 需要及时可见性
场景2:双重检查锁定(DCL)
单例模式的正确实现:
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第1次检查(读volatile)
synchronized (Singleton.class) {
if (instance == null) { // 第2次检查
instance = new Singleton(); // 写volatile
// volatile禁止重排序:
// 1. 分配内存
// 2. 初始化对象
// 3. 赋值给instance
}
}
}
return instance;
}
}
如果不加volatile会怎样:
instance = new Singleton();
// 可能重排序为:
// 1. 分配内存
// 2. instance指向内存(未初始化!)
// 3. 初始化对象
// 另一个线程:
if (instance != null) { // 看到instance!=null
instance.foo(); // 但对象未初始化!NPE!
}
场景3:独立观察(Independent Observation)
public class UserManager {
private volatile User currentUser;
// 写线程
public void login(User user) {
currentUser = user; // 发布引用
}
// 读线程
public User getCurrentUser() {
return currentUser; // 读取引用
}
}
关键点:
User对象是不可变的(或已正确发布)- 只发布引用,不修改对象内容
- volatile保证引用的可见性
场景4:开销较低的"读-写锁"策略
public class CheesyCounter {
private volatile int value;
public int getValue() {
return value; // 读volatile,无锁
}
public synchronized void increment() {
value++; // 写操作需要加锁(保证原子性)
}
}
适用条件:
- 读操作远多于写操作
- 写操作需要synchronized保证原子性
- 读操作可以用volatile提高性能
场景5:触发初始化(Initialization On Demand)
public class LazyInitialization {
private volatile Helper helper;
public Helper getHelper() {
Helper h = helper; // 读volatile(只读一次,减少开销)
if (h == null) {
synchronized (this) {
h = helper;
if (h == null) {
helper = h = new Helper(); // 写volatile
}
}
}
return h;
}
}
五、volatile不能做什么?
5.1 不能保证原子性
经典错误:
public class Counter {
private volatile int count = 0;
public void increment() {
count++; // 不是原子操作!
}
}
问题分析:
count++;
// 实际上是3个操作:
// 1. temp = count (读)
// 2. temp = temp + 1 (修改)
// 3. count = temp (写)
// 多线程执行时:
线程1: temp1 = count (读到0)
线程2: temp2 = count (读到0)
线程1: count = temp1 + 1 (写入1)
线程2: count = temp2 + 1 (写入1) ← 丢失一次更新!
解决方案:
// 方案1:synchronized
private int count = 0;
public synchronized void increment() {
count++;
}
// 方案2:AtomicInteger
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
5.2 不能建立复合操作的原子性
check-then-act模式:
public class UnsafeRange {
private volatile int lower = 0;
private volatile int upper = 10;
public void setRange(int lower, int upper) {
if (lower > upper) {
throw new IllegalArgumentException();
}
this.lower = lower; // 写volatile
this.upper = upper; // 写volatile
// 两个写操作之间可能有其他线程读取
}
public boolean isInRange(int value) {
return value >= lower && value <= upper;
// 可能读到不一致的lower和upper!
}
}
时序分析:
时刻1: 线程1执行setRange(5, 15)
线程1: lower = 5
时刻2: 线程2执行isInRange(8)
线程2: 读到lower=5, upper=10(旧值)
线程2: 返回true
时刻3: 线程1继续
线程1: upper = 15
解决方案:
public synchronized void setRange(int lower, int upper) {
if (lower > upper) {
throw new IllegalArgumentException();
}
this.lower = lower;
this.upper = upper;
}
public synchronized boolean isInRange(int value) {
return value >= lower && value <= upper;
}
5.3 不能实现互斥
错误示例:
public class WrongLock {
private volatile boolean locked = false;
public void lock() {
while (locked) {
// 自旋等待
}
locked = true; // 设置锁
}
public void unlock() {
locked = false;
}
}
问题:
线程1: while (locked) {...} 检查通过
线程2: while (locked) {...} 也检查通过!
线程1: locked = true
线程2: locked = true
→ 两个线程都获取了锁!
六、volatile vs synchronized
6.1 对比表
| 特性 | volatile | synchronized |
|---|---|---|
| 原子性 | ❌ 不保证 | ✅ 保证 |
| 可见性 | ✅ 保证 | ✅ 保证 |
| 有序性 | ✅ 部分保证(禁止重排序) | ✅ 完全保证 |
| 适用场景 | 单一变量读写 | 复合操作 |
| 性能 | ⚡ 轻量级 | 🐢 重量级 |
| 阻塞 | 不阻塞 | 可能阻塞 |
6.2 性能测试
public class PerformanceTest {
private volatile long volatileCount = 0;
private long syncCount = 0;
// volatile写入
public void volatileIncrement() {
volatileCount++; // 不安全但测试性能
}
// synchronized写入
public synchronized void syncIncrement() {
syncCount++;
}
public static void main(String[] args) {
PerformanceTest test = new PerformanceTest();
int threads = 4;
int iterations = 10_000_000;
// 测试volatile
long start = System.currentTimeMillis();
// ... 多线程执行volatileIncrement
long volatileTime = System.currentTimeMillis() - start;
// 测试synchronized
start = System.currentTimeMillis();
// ... 多线程执行syncIncrement
long syncTime = System.currentTimeMillis() - start;
System.out.println("volatile: " + volatileTime + "ms");
System.out.println("synchronized: " + syncTime + "ms");
}
}
实测结果(8核CPU):
volatile: 1250ms
synchronized: 8900ms
性能差距:7倍
6.3 什么时候用volatile?
使用volatile的条件(必须同时满足):
- ✅ 对变量的写入不依赖当前值
- ✅ 该变量不会与其他状态变量一起纳入不变性条件
- ✅ 访问变量时不需要加锁
示例判断:
// ✅ 适合用volatile
private volatile boolean shutdownRequested;
public void shutdown() {
shutdownRequested = true; // 不依赖当前值
}
// ❌ 不适合用volatile
private volatile int count = 0;
public void increment() {
count++; // 依赖当前值
}
// ❌ 不适合用volatile
private volatile int lower = 0;
private volatile int upper = 10;
// 两个变量有不变性条件:lower <= upper
七、volatile的常见陷阱
陷阱1:volatile数组
volatile int[] arr = new int[10];
// 问题:
arr[0] = 1; // 数组元素的修改不是volatile的!
正确理解:
- volatile修饰的是数组引用
- 数组元素的读写不是volatile的
解决方案:
// 使用AtomicIntegerArray
AtomicIntegerArray arr = new AtomicIntegerArray(10);
arr.set(0, 1); // 原子操作
陷阱2:volatile对象的字段
class User {
String name;
}
volatile User user;
// 问题:
user.name = "Alice"; // name的修改不是volatile的!
正确理解:
- volatile保证
user引用的可见性 - 不保证
user.name的可见性
解决方案:
// 方案1:不可变对象
final class User {
final String name;
User(String name) { this.name = name; }
}
// 方案2:替换整个对象
User newUser = new User("Alice");
user = newUser; // volatile写
陷阱3:Long和Double的特殊性
问题:
// 64位的long和double,读写不是原子的
private long value = 0;
// 线程1
value = 0x1234567890ABCDEFL;
// 线程2
long temp = value; // 可能读到高32位和低32位的不一致组合
解决方案:
private volatile long value = 0; // volatile保证64位读写原子性
八、总结
8.1 核心要点
volatile的两大作用
- 保证可见性:写立即刷新,读总是最新
- 禁止重排序:通过内存屏障实现
底层实现
- x86:
lock前缀指令 - ARM:DMB内存屏障指令
- 开销远小于synchronized
- x86:
适用场景
- 状态标志
- 双重检查锁定
- 独立观察
- 一次性安全发布
不适用场景
- 复合操作(count++)
- 多个变量的不变性条件
- 需要互斥的场景
性能特点
- 比synchronized快5-10倍
- 不会导致线程阻塞
- 适合读多写少的场景
8.2 最佳实践
优先使用更高层的工具
// 推荐 AtomicInteger count = new AtomicInteger(); // 不推荐 volatile int count = 0;正确的DCL单例
private static volatile Singleton instance;状态标志模式
private volatile boolean running = true;避免复杂的volatile逻辑
- 如果逻辑复杂,用synchronized
- 简单读写才用volatile
8.3 思考题
- 为什么volatile能保证long/double的原子性,但不能保证i++的原子性?
- volatile是否会影响CPU缓存的命中率?
- 如何用JMH测试volatile的性能开销?
8.4 下一篇预告
在理解了volatile的轻量级同步机制后,下一篇我们将深入学习synchronized原理 —— 重量级锁是如何工作的,以及它如何演进优化的。
扩展阅读
JSR-133:Java内存模型规范
Intel手册:Intel® 64 and IA-32 Architectures Software Developer’s Manual
ARM手册:ARM Architecture Reference Manual
论文:
- “The Java Memory Model” - Manson, Pugh, Adve
- “Memory Barriers: a Hardware View for Software Hackers” - Paul E. McKenney
工具:
- JCStress:测试volatile正确性
- JMH:性能基准测试
- hsdis:查看生成的汇编代码
系列文章:
- 上一篇:Java并发11:happens-before原则
- 下一篇:Java并发13:synchronized原理与使用 (即将发布)