Java中的volatile关键字
volatile 是 Java 中用于修饰变量的轻量级同步关键字,核心作用是保证可见性 与有序性 ,但不保证原子性
1.保证可见性
- 当一个线程修改 volatile 变量后,JVM 会强制将新值立即刷新到主内存,而非仅停留在线程本地缓存。
- 其他线程读取该变量时,会直接从主内存加载最新值,而非使用本地缓存的旧值,避免 "缓存不一致" 导致的可见性问题。
- 可见性的核心是「多线程环境下,一个线程对变量的修改,其他线程能立刻看到」
- 解决了线程本地缓存和主内存的同步问题
java
private boolean flag = false;
// 线程 A
new Thread(() -> {
while(!flag) {
// 一直循环,直到 flag 为 true
}
}).start();
// 线程 B
new Thread(() -> {
flag = true;
System.out.println("flag 已设为 true");
}).start();
如果B修改了flag 并且写回主存 但是A可能一直在读取自己的值陷入了死循环
- 线程 B 的修改没有被强制同步到主内存;
- 线程 A 没有被强制去主内存读取最新值。
这就叫可见性丢失,总是一句话,你修改了某个值,立马就会和其他线程同步你修改的,防止你的数据是错误或者过时的!消除工作内存和主内存的延迟
2.禁止指令重排序
- 编译器与 CPU 为优化性能可能对指令重排,但 volatile 通过插入内存屏障(Memory Barrier)阻止特定重排,确保代码执行顺序与预期一致。
volatile的核心作用之一就是禁止指令重排序(另一个是保证可见性)
指令重排序例子(双重检查锁单例)
1. 有问题的 DCL 单例(指令重排序导致线程不安全)
java
public class Singleton {
// 未加 volatile,可能发生指令重排序
private static Singleton instance;
private Singleton() {} // 私有构造,防止外部实例化
public static Singleton getInstance() {
// 第一次检查:避免每次获取实例都加锁
if (instance == null) {
// 加锁,保证多线程下只有一个线程能进入
synchronized (Singleton.class) {
// 第二次检查:防止多个线程等待锁后重复创建实例
if (instance == null) {
// 这里会发生指令重排序!
instance = new Singleton();
}
}
}
return instance;
}
}
instance = new Singleton();这行代码看似是一步,实际 JVM 会拆分成 3 步:
- 分配内存空间(
memory = allocate())- 初始化对象(
ctorInstance(memory))- 将
instance指向分配的内存地址(instance = memory)JVM 可能对步骤 2 和 3 进行重排序,变成:
- 分配内存空间
- 将
instance指向内存地址(此时对象还未初始化!)- 初始化对象
此时如果有另一个线程进入
getInstance()方法,发现instance != null,就会直接返回这个未初始化完成的对象,导致程序出错。
如何修改?
加上volatile,禁止指令重排序 private static volatile Singleton instance;
volatile 做了什么?
volatile会在指令序列中插入内存屏障:
- 禁止写操作(步骤 3)重排序到初始化操作(步骤 2)之前;
- 保证一个线程对
instance的写操作,对其他线程的读操作可见。
3. 不保证原子性
- 单个 volatile 变量的读写是原子操作,但复合操作(如 i++、i += 1)包含 "读 - 改 - 写" 三步,非原子,多线程并发仍可能出现竞态条件,需用 synchronized 或 Atomic 类补充。
原子性 = 不可分割性。一个操作要么完全执行完,要么完全不执行,中间不会被其他线程打断
像count++、count += 1、count = count + 1,这些看似是一行代码
实际在底层会拆成 3 个独立步骤:
- 读:从主内存读取 count 的当前值到线程工作内存;
- 改:在工作内存中把 count 值 +1;
- 写:把修改后的值写回主内存。
这 3 步是分开执行的,volatile 只能保证 "读的时候读最新值""写的时候立刻刷回主内存",但无法保证这 3 步作为一个整体不被打断。
假设有线程 A 和线程 B 同时执行 count++,原本 count=0:
- 线程 A 读:从主内存拿到 count=0;
- 线程 B 读:也从主内存拿到 count=0(volatile 保证读的是最新值,但此时还没修改);
- 线程 A 改:0+1=1;
- 线程 B 改:0+1=1;
- 线程 A 写:把 1 刷回主内存;
- 线程 B 写:把 1 刷回主内存(覆盖了线程 A 的结果)。
原本期望 2 次 ++ 后 count=2,但实际结果是 1------ 这就是原子性丢失 ,因为 count++ 的 3 步被打断了,两个线程的修改互相覆盖。
保证原子性一般用 synchronized 或 Atomic 类补充