一、什么是volatile?🔮
// 一个神奇的关键字
private volatile boolean flag = false;
// 线程1
flag = true;
// 线程2
while (!flag) {
// 等待flag变化
}
一句话总结:
volatile是Java提供的轻量级同步机制,保证变量的可见性和有序性,但不保证原子性!
二、可见性问题:看不见的修改 👻
问题场景
public class VisibilityProblem {
private boolean flag = false; // 注意:没有volatile
// 线程1:写操作
public void writer() {
flag = true; // ①修改flag
System.out.println("flag已设置为true");
}
// 线程2:读操作
public void reader() {
while (!flag) { // ②读取flag
// 空转等待
}
System.out.println("检测到flag变化!");
}
public static void main(String[] args) {
VisibilityProblem problem = new VisibilityProblem();
// 线程2先启动
new Thread(problem::reader).start();
// 等1秒后,线程1修改flag
Thread.sleep(1000);
new Thread(problem::writer).start();
}
}
预期结果:
flag已设置为true
检测到flag变化!
实际结果:
flag已设置为true
(线程2陷入死循环,永远不会输出!)💀
为什么会这样?内存模型揭秘 🏗️
Java内存模型(JMM)
┌─────────────────────────────────────────┐
│ 主内存 (Main Memory) │
│ ┌────────────────────────────────┐ │
│ │ flag = true (最新值) │ │
│ └────────────────────────────────┘ │
└─────────────────────────────────────────┘
↑ ↑
│ 不及时同步! │
│ │
┌────┴───┐ ┌────┴───┐
│ 工作内存 │ │ 工作内存 │
│(Thread1)│ │(Thread2)│
│flag=true│ │flag=false│ ← 还是旧值!
└────────┘ └─────────┘
线程1 线程2
详细流程:
1. 初始状态:
主内存:flag = false
线程1工作内存:flag = false
线程2工作内存:flag = false
2. 线程1修改flag:
线程1工作内存:flag = true ← 修改了
主内存:flag = false ← 还没同步
线程2工作内存:flag = false ← 还是旧值
3. 线程1刷新到主内存(不确定何时):
主内存:flag = true ← 已更新
线程2工作内存:flag = false ← 还是旧值!
4. 线程2一直读取自己的工作内存:
线程2工作内存:flag = false ← 一直是旧值
(死循环!)💀
生活比喻:
主内存 = 公司公告栏 📋
工作内存 = 每个人的小本本 📓
场景:老板(线程1)改了下班时间
老板在公告栏写:6点下班 → 5点下班 ✅
员工小明(线程2):
- 早上来时抄了公告(6点下班)到小本本
- 工作时只看自己的小本本
- 一直等到6点!
- 不知道公告栏已经改了!😭
这就是可见性问题!
解决方案:volatile
private volatile boolean flag = false; // ✅ 加上volatile
// 线程1
flag = true;
// volatile保证:
// 1. 立即刷新到主内存
// 2. 通知其他CPU缓存失效
// 线程2
while (!flag) {
// volatile保证:
// 1. 每次都从主内存读取
// 2. 不会使用缓存的旧值
}
加上volatile后的内存模型:
1. 线程1修改flag = true
↓ 立即触发
├─ 刷新到主内存 ✅
└─ 通知其他CPU缓存失效 ✅
2. 线程2读取flag
↓ 发现缓存失效
└─ 从主内存重新读取 ✅
三、有序性问题:指令重排序的陷阱 🔀
什么是指令重排序?
为了提高性能,编译器和CPU可能会改变指令执行顺序!
// 源代码:
int a = 1; // ①
int b = 2; // ②
int c = a + b; // ③
// 实际执行顺序可能是:
int a = 1; // ①
int c = a + 2; // ③(提前计算,b还没赋值)
int b = 2; // ②
// 单线程没问题,多线程就出问题了!
经典案例:双重检查锁定(DCL)的坑
// ❌ 错误的单例模式
public class Singleton {
private static Singleton instance; // 没有volatile
public static Singleton getInstance() {
if (instance == null) { // ① 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // ② 第二次检查
instance = new Singleton(); // ③ 创建对象
}
}
}
return instance;
}
}
问题在哪?
new Singleton()实际上不是原子操作,分为三步:
instance = new Singleton();
// 实际执行:
memory = allocate(); // ① 分配内存空间
ctorInstance(memory); // ② 初始化对象
instance = memory; // ③ 设置instance指向内存地址
// 💣 指令重排序后可能是:
memory = allocate(); // ① 分配内存空间
instance = memory; // ③ 设置instance指向内存地址(提前了!)
ctorInstance(memory); // ② 初始化对象(还没执行!)
多线程场景下的问题:
时间线:
线程A:执行getInstance()
├─ instance == null (true)
├─ 获取锁
├─ instance == null (true)
├─ memory = allocate() ① 分配内存
├─ instance = memory ③ 指向内存(但还没初始化!)
│
│ 【此时线程B介入】
│
├─ 线程B:执行getInstance()
│ ├─ instance == null (false) ← 不为null了!
│ └─ return instance ← 💣 返回了一个未初始化的对象!
│
└─ ctorInstance(memory) ② 初始化对象(晚了!)
生活比喻:
买房子🏠:
正常顺序:
① 盖房子
② 装修房子
③ 给你钥匙
重排序后:
① 盖房子
③ 给你钥匙(房子还没装修!)
② 装修房子
结果:你拿到钥匙进门,发现是毛坯房!😱
解决方案:volatile禁止重排序
// ✅ 正确的单例模式
public class Singleton {
private static volatile Singleton instance; // 加上volatile
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
// volatile保证:
// ① 内存分配
// ② 初始化对象
// ③ 设置引用
// 这三步不会被重排序!✅
}
}
}
return instance;
}
}
四、volatile的底层实现:内存屏障 🛡️
什么是内存屏障?
**内存屏障(Memory Barrier)**是一种CPU指令,用于控制内存操作的顺序。
四种内存屏障
| 屏障类型 | 说明 | 示例 |
|---|---|---|
| LoadLoad | Load1; LoadLoad; Load2 | 确保Load1在Load2之前完成 |
| StoreStore | Store1; StoreStore; Store2 | 确保Store1在Store2之前完成 |
| LoadStore | Load1; LoadStore; Store2 | 确保Load1在Store2之前完成 |
| StoreLoad | Store1; StoreLoad; Load2 | 确保Store1在Load2之前完成 |
volatile写操作的内存屏障
volatile int v = 0;
// 写操作:v = 1;
普通操作
普通操作
StoreStore屏障 ← 禁止上面的普通写与下面的volatile写重排序
volatile写
StoreLoad屏障 ← 禁止上面的volatile写与下面的volatile读/写重排序
volatile读操作的内存屏障
// 读操作:int i = v;
LoadLoad屏障 ← 禁止下面的普通读与上面的volatile读重排序
volatile读
LoadStore屏障 ← 禁止下面的普通写与上面的volatile读重排序
普通操作
普通操作
字节码层面:lock指令
volatile int v = 0;
// 写操作会被编译为:
0x01a3de24: movb $0×0,0×1104800(%esi);
0x01a3de2b: lock addl $0×0,(%esp); ← lock前缀指令
lock指令的作用:
- ✅ 将当前处理器缓存行的数据写回主内存
- ✅ 使其他处理器的缓存行失效(MESI协议)
- ✅ 提供内存屏障功能
五、原子性问题:volatile的局限 ⚠️
volatile不保证原子性!
public class VolatileAtomicTest {
private volatile int count = 0; // 即使有volatile
public void increment() {
count++; // ❌ 这不是原子操作!
}
public static void main(String[] args) throws Exception {
VolatileAtomicTest test = new VolatileAtomicTest();
// 10个线程,每个+1000次
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(() -> {
for (int j = 0; j < 1000; j++) {
test.increment();
}
}));
}
threads.forEach(Thread::start);
threads.forEach(t -> t.join());
System.out.println("count = " + test.count);
// 预期:10000
// 实际:9856(每次不一样,但小于10000)❌
}
}
为什么?
count++实际上是三个操作:
count++;
// 实际执行:
int temp = count; // ① 读取
temp = temp + 1; // ② 加1
count = temp; // ③ 写回
// volatile只保证每一步的可见性,但不保证三步的原子性!
并发场景:
初始:count = 0
时间点1:
线程A:读取count = 0 ①
线程B:读取count = 0 ① (同时读取)
时间点2:
线程A:temp = 0 + 1 = 1 ②
线程B:temp = 0 + 1 = 1 ② (都是1)
时间点3:
线程A:count = 1 ③ (写入1)
线程B:count = 1 ③ (写入1,覆盖了!)
结果:count = 1(丢失了一次+1操作!)💥
生活比喻:
银行账户💰,初始余额0元
你和配偶同时存钱:
时间1:
你:查看余额(0元)
配偶:查看余额(0元)
时间2:
你:计算新余额(0 + 100 = 100)
配偶:计算新余额(0 + 200 = 200)
时间3:
你:写入余额(100元)
配偶:写入余额(200元)← 覆盖了你的操作
结果:余额200元,丢失了你的100元!😭
解决方案
1️⃣ 使用synchronized
private int count = 0;
public synchronized void increment() {
count++; // ✅ synchronized保证原子性
}
2️⃣ 使用AtomicInteger
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // ✅ CAS保证原子性
}
3️⃣ 使用LongAdder(高并发场景)
private LongAdder count = new LongAdder();
public void increment() {
count.increment(); // ✅ 分段累加,性能更高
}
六、volatile的适用场景 ✅
场景1:状态标志
public class ServerThread extends Thread {
private volatile boolean running = true; // ✅ 适合
@Override
public void run() {
while (running) {
// 处理请求
}
// 清理资源
}
public void shutdown() {
running = false; // 停止线程
}
}
场景2:双重检查锁定(DCL)
public class Singleton {
private static volatile Singleton instance; // ✅ 必须
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
场景3:读多写少的状态
public class Config {
private volatile Map<String, String> config; // ✅ 适合
// 写操作:偶尔发生
public void refresh() {
Map<String, String> newConfig = loadFromDB();
config = newConfig; // 原子操作,直接替换引用
}
// 读操作:频繁发生
public String get(String key) {
return config.get(key); // 总是读到最新配置
}
}
场景4:一次性安全发布
public class SafePublish {
private volatile Resource resource;
public void init() {
Resource temp = new Resource();
temp.initialize(); // 初始化
resource = temp; // ✅ volatile保证完全初始化后才发布
}
public Resource getResource() {
return resource;
}
}
七、volatile vs 其他同步机制 ⚖️
| 特性 | volatile | synchronized | Lock | Atomic |
|---|---|---|---|---|
| 可见性 | ✅ | ✅ | ✅ | ✅ |
| 有序性 | ✅ | ✅ | ✅ | ✅ |
| 原子性 | ❌ | ✅ | ✅ | ✅ |
| 性能 | 最高 | 中 | 中 | 高 |
| 阻塞 | ❌ | ✅ | ✅ | ❌ |
| 适用场景 | 状态标志 | 复杂同步 | 高级功能 | 简单计数 |
八、常见误区 ⚠️
误区1:volatile万能论
// ❌ 错误:以为volatile能保证线程安全
private volatile int count = 0;
public void increment() {
count++; // ❌ 不安全!
}
// ✅ 正确:
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
误区2:volatile比synchronized快很多
// JDK 6+,synchronized做了大量优化
// 简单场景性能差不多
// volatile读写:几乎无开销
// synchronized(偏向锁):第一次CAS,之后几乎无开销
误区3:volatile能替代锁
// ❌ 错误:复杂操作用volatile
private volatile List<String> list = new ArrayList<>();
public void add(String item) {
list.add(item); // ❌ ArrayList不是线程安全的!
}
// ✅ 正确:
private final Object lock = new Object();
private List<String> list = new ArrayList<>();
public void add(String item) {
synchronized (lock) {
list.add(item);
}
}
九、面试应答模板 🎤
面试官:说说volatile如何保证可见性和有序性?为什么不能保证原子性?
你的回答:
可见性实现:
- volatile写操作会在之后插入StoreLoad内存屏障,强制刷新到主内存
- volatile读操作会在之前插入LoadLoad屏障,从主内存读取最新值
- 底层通过lock指令实现,触发MESI缓存一致性协议,使其他CPU缓存失效
有序性实现:
- volatile通过内存屏障禁止指令重排序
- 写操作:StoreStore + StoreLoad屏障
- 读操作:LoadLoad + LoadStore屏障
- 经典应用:DCL单例模式,防止对象半初始化
不保证原子性:
- volatile只保证单个读/写操作的原子性
- 复合操作(如i++)不是原子的,分为读-改-写三步
- 多线程可能在三步之间交替执行,导致数据不一致
举例:
count++; // 读取、加1、写回,三步不是原子的
解决方案:
- 使用synchronized
- 使用Atomic类(CAS)
- 使用Lock
十、总结 🎯
volatile特性总结:
✅ 可见性
└─ 修改立即刷新到主内存
└─ 读取总是从主内存读
✅ 有序性
└─ 禁止指令重排序
└─ 内存屏障实现
❌ 原子性
└─ 只保证单个操作
└─ 复合操作不保证
适用场景:
├─ 状态标志
├─ DCL单例
├─ 读多写少
└─ 一次性发布
不适用场景:
├─ 计数器(用Atomic)
├─ 复杂同步(用synchronized/Lock)
└─ 集合操作(用并发容器)
记忆口诀:
volatile轻量级,
可见有序它都行,
唯独原子性不保,
count++要小心!
状态标志最适合,
单例DCL也要用,
内存屏障是底层,
lock指令来实现!🎵
核心要点:
- ✅ volatile保证可见性(内存屏障)
- ✅ volatile保证有序性(禁止重排序)
- ❌ volatile不保证原子性(复合操作不行)
- ✅ 适合状态标志、DCL、读多写少场景
- ✅ 底层:内存屏障 + lock指令 + MESI协议