volatile 关键字笔记

一、什么是 volatile?

volatile 是 Java 中的一个关键字,用于修饰变量,保证变量的 可见性 和 禁止指令重排序,但不保证原子性。

二、核心特性

  1. 可见性

    当一个变量被 volatile 修饰时,线程对该变量的修改会 立即被其他线程看到

    • 原理:线程修改 volatile 变量后,会强制将修改后的值刷新到主内存;其他线程读取时,会直接从主内存加载最新值(而非线程本地缓存)。
    • 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class VolatileDemo {
volatile boolean flag = false;

void setFlag() {
flag = true; // 修改后立即刷新到主内存
}

void checkFlag() {
while (!flag) {
// 每次循环都会从主内存读取flag最新值
}
System.out.println("flag已变为true");
}
}

线程 A 调用 setFlag() 后,线程 B 的 checkFlag() 会立即感知到 flag 的变化并退出循环。

  1. 禁止指令重排序

    编译器或 CPU 为优化性能,可能会对代码指令重排序(不影响单线程结果,但可能破坏多线程逻辑)。volatile 会禁止这种重排序,保证代码执行顺序与编写顺序一致。

    • 典型场景:单例模式的双重检查锁(避免未初始化完成的对象被引用):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Singleton {
private static volatile Singleton instance; // 必须加volatile

private Singleton() {}

static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 禁止重排序
}
}
}
return instance;
}
}

若不加 volatileinstance = new Singleton() 可能被拆分为「分配内存→引用指向内存→初始化对象」,重排序后可能导致其他线程拿到未初始化的对象。

三、不保证原子性

volatile 不能保证复合操作的原子性(如 i++,本质是「读 - 改 - 写」三步)。

  • 反例:多线程自增 volatile 变量,结果可能小于预期:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class VolatileAtomicDemo {
volatile int count = 0;

void increment() {
count++; // 非原子操作,volatile无法保证线程安全
}

public static void main(String[] args) throws InterruptedException {
VolatileAtomicDemo demo = new VolatileAtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo.increment();
}
}).start();
}
Thread.sleep(1000);
System.out.println(demo.count); // 结果可能小于10000
}
}
解决:需配合 `synchronized` 或 `AtomicInteger` 保证原子性。

四、适用场景

  1. 状态标记:如示例中的 flag,用于线程间简单的状态传递(启动 / 停止信号)。
  2. 双重检查锁单例:禁止指令重排序,保证单例对象正确初始化。
  3. 简单变量的读写:仅涉及单个变量的读和写(无复合操作)。

五、总结

  • volatile 是轻量级同步机制,性能优于 synchronized,但功能有限。
  • 核心作用:保证可见性、禁止指令重排序,不保证原子性。
  • 适合简单的线程间状态同步,复杂场景需结合锁或原子类使用。

我们用生活化的例子,把 volatile 讲得明明白白:

一、先搞懂:没有 volatile 会出啥问题?

电脑里的线程读取变量时,为了速度快,会把主内存的变量 “拷贝” 一份到自己的 “本地缓存”(比如 CPU 缓存)。

就像你抄作业时,把课本(主内存)的内容抄到笔记本(本地缓存)上,之后直接看笔记本,不回头看课本了。

这会导致两个问题:

  1. 看不见别人的修改:如果别人改了课本内容,你没翻课本,永远不知道最新内容;
  2. 自己的操作顺序乱了:老师布置 “先写数学再写语文”,你为了省时间先写了语文(对应 CPU 指令重排序),单看你自己没问题,但如果和同学协作(多线程)就会出乱子。

二、volatile 到底是干啥的?

volatile 就像给变量加了两个 “强制规则”,解决上面的问题:

1. 规则 1:保证 “可见性”—— 改了必须让所有人知道

被 volatile 修饰的变量,线程修改它时,必须立刻把新值写回课本(主内存);其他线程读这个变量时,必须直接从课本(主内存)读,不能看自己的笔记本(本地缓存)

举个例子:

  • 线程 A 改了 volatile 变量 flag = true,马上写回主内存;
  • 线程 B 之前缓存的 flag 是 false,但因为加了 volatile,它会重新去主内存读,立刻知道 flag 变了。

没有 volatile 的话,线程 A 改完可能先存在自己的缓存里,线程 B 永远看不到,就会一直卡着。

2. 规则 2:禁止 “指令重排序”—— 必须按规矩来

被 volatile 修饰的变量,涉及它的操作不能乱序执行

比如单例模式里的 instance = new Singleton(),本质是 3 步:① 分配内存 ② 初始化对象 ③ 把变量指向内存。

没有 volatile 时,CPU 可能优化成 ①→③→②,导致其他线程拿到 “还没初始化好的对象”;加了 volatile 后,就必须按 ①→②→③ 执行,避免出错。

三、volatile 的 “短板”:不保证原子性

volatile 只能管 “读” 和 “写单个变量” 的正确性,但管不了 “多步操作”。

比如 count++,看似简单,实际是 3 步:① 读 count 的值 ② 加 1 ③ 写回新值。这 3 步中间可能被其他线程打断,导致结果出错。

举个例子:两个线程同时执行 count++(初始值 0):

  1. 线程 A 读 count=0,还没来得及加 1,线程 B 也读 count=0
  2. 线程 A 加 1 写回 1,线程 B 加 1 也写回 1
  3. 最终 count=1,而不是预期的 2

解决办法:这种复合操作,得用 synchronized 锁或者 AtomicInteger 这类原子类。

四、什么时候用 volatile?

  1. 状态标记:比如用 volatile boolean isStop 控制线程启动 / 停止,简单的线程间信号传递;
  2. 单例模式双重检查锁:必须加 volatile 禁止重排序,避免拿到未初始化的对象;
  3. 简单变量读写:只涉及单个变量的读和写,没有多步复合操作。

一句话总结

volatile 是个 “轻量级同步工具”,专门解决 “线程看不到变量最新值” 和 “操作顺序乱了” 的问题,但管不了多步操作的原子性,复杂场景得搭配锁或原子类使用。