1. 基础概念
1.1 什么是 volatile?
volatile
是 Java 中的一个关键字,用于修饰变量。它保证了变量的可见性 和有序性 ,但不保证原子性。
java
public class VolatileExample {
// 普通变量
private int normalVar = 0;
// volatile 变量
private volatile int volatileVar = 0;
// volatile 引用类型
private volatile Object obj = new Object();
}
1.2 volatile 的三大特性
- 可见性(Visibility):当一个线程修改了 volatile 变量的值,其他线程能够立即看到这个修改。
- 有序性(Ordering):禁止指令重排序优化。
- 非原子性(Non-Atomicity):volatile 不能保证复合操作的原子性。
2. volatile 的底层实现机制
2.1 JMM 内存划分
JMM 中将内存划分为主内存和工作内存。
线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内 存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
点击阅读:JVM(一)------ JVM内存管理
2.2 指令重排序问题
为了提高程序执行效率,编译器和 CPU 可以改变指令的执行顺序,单线程程序的结果不变。目的是利用 CPU 并行能力、流水线优化、减少内存访问延迟,提高性能。
java
/**
* 【底层原理】编译器和 CPU 为了优化性能会重新排列指令执行顺序
*/
public class ReorderingProblem {
private int a = 0;
private boolean flag = false;
// 线程 1 执行
public void writer() {
a = 1; // 操作 1
flag = true; // 操作 2
// 【问题】编译器或 CPU 可能重排序为:
// flag = true; // 操作 2 先执行
// a = 1; // 操作 1 后执行
}
// 线程 2 执行
public void reader() {
if (flag) { // 如果看到 flag = true
// 【问题】可能看到 a = 0(重排序导致)
System.out.println("a = " + a);
}
}
}
2.3 内存屏障(Memory Barrier)
内存屏障是一种指令级机制,用于控制 CPU 或编译器的指令执行顺序,并保证多线程访问共享变量的可见性和顺序性。
java
/**
* volatile 变量的读写操作会插入内存屏障
* 【底层原理】JVM 会在 volatile 变量操作前后插入特定的内存屏障指令。通过内存屏障强制刷新缓存,保证可见性
*/
public class MemoryBarrierExample {
private volatile boolean flag = false;
private int data = 0;
public void writer() {
data = 42; // 1. 普通写操作
// StoreStore 屏障 - 确保上面的写操作在 volatile 写之前完成
flag = true; // 2. volatile 写操作
// StoreLoad 屏障 - 确保 volatile 写操作立即刷新到主内存
}
public void reader() {
// LoadLoad 屏障 - 确保 volatile 读操作从主内存获取最新值
if (flag) { // 1. volatile 读操作
// LoadStore 屏障 - 确保后续读操作能看到最新的数据
System.out.println(data); // 2. 能够看到 data = 42
}
}
}
四种内存屏障类型:
屏障类型 | 位置 | 作用 |
---|---|---|
StoreStore | volatile 写之前 | 确保该线程之前的普通写操作(如 data=42)都已经写入工作内存或刷新到主内存,不能被重排到 volatile 写之后 |
StoreLoad | volatile 写之后 | 保证 volatile 写立即刷新到主内存,并阻止 volatile 写与随后的读/写重排 |
LoadLoad | volatile 读之前 | 确保 volatile 读之前的读操作完成,防止读操作重排到 volatile 读之后 |
LoadStore | volatile 读之后 | 确保 volatile 读完成后,后续读/写操作不会被重排到 volatile 读之前 |
happens-before 原则:所有在 volatile 写之前的操作都对后续的 volatile 读可见
2.4 不保证原子性
复合操作 = 多步骤操作,例如:
java
counter++; // 等价于 read -> modify -> write
Step 1: 从主内存/工作内存读 counter
Step 2: 自增操作(+1)
Step 3: 写回工作内存/主内存
问题 :多个线程同时执行 counter++
,可能出现:
- 线程 A 读到 5
- 线程 B 也读到 5
- A 写回 6
- B 写回 6
结果:虽然两个线程都执行了 ++,counter 只增加了 1 → 数据丢失
volatile 只保证每次读/写最新可见,不保证多个操作的不可分割性。
3. 应用场景
3.1 单例模式(双重检查锁定)
为什么 instance 必须用 volatile 修饰?在双重检查锁定单例模式中,instance 必须用 volatile 修饰,否则可能因为 指令重排序 导致返回"未初始化完成"的对象,从而破坏单例的线程安全性。
java
/**
* 【经典应用】双重检查锁定单例模式
*/
public class Singleton {
// 必须使用 volatile 修饰
// 防止指令重排序导致的半初始化对象问题
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
// 【问题】对象创建分为三个步骤:
// 1. 分配内存空间
// 2. 初始化对象
// 3. 将引用指向内存空间
//
// 【重排序风险】步骤 2 和 3 可能被重排序
// 【后果】其他线程可能看到未完全初始化的对象
instance = new Singleton(); // 可能发生重排序
}
}
}
return instance;
}
}
/**
* 【错误示例】不使用 volatile 的问题
*/
class ProblematicSingleton {
private static ProblematicSingleton instance; // 没有 volatile
public static ProblematicSingleton getInstance() {
if (instance == null) {
synchronized (ProblematicSingleton.class) {
if (instance == null) {
// 【问题场景】
// 线程 A:执行到步骤 3,instance 不为 null,但对象未初始化
// 线程 B:看到 instance != null,直接返回未初始化的对象
// 正常顺序:分配内存 → 初始化 → 引用指向
// 重排序后的顺序:分配内存 → 引用指向(instance != null) → 初始化
// 【结果】程序崩溃或产生不可预期的行为
instance = new ProblematicSingleton();
}
}
}
return instance;
}
}
3.2 状态标志
java
/**
* 【应用场景】线程间状态通信
* 【适用条件】简单的布尔状态标志
*/
public class StatusFlag {
// 【用途】控制线程的启动和停止
private volatile boolean running = true;
private volatile boolean initialized = false;
public void startWorker() {
new Thread(() -> {
// 等待初始化完成
while (!initialized) {
// 【volatile 读】确保能及时看到初始化完成的信号
Thread.yield();
}
// 执行工作循环
while (running) {
// 【volatile 读】确保能及时响应停止信号
doWork();
}
cleanup();
}).start();
}
public void initialize() {
// 执行初始化逻辑
setupResources();
// 【volatile 写】通知工作线程初始化完成
initialized = true;
}
public void stop() {
// 【volatile 写】发送停止信号
running = false;
}
private void doWork() {
// 模拟工作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}