volatile关键字用于保证多线程环境下共享变量的可见性和禁止指令重排序,通过内存屏障确保写操作立即刷新到主内存、读操作强制从主内存获取最新值,并建立happens-before关系以保障操作顺序与可见性;它适用于状态标志位、DCL单例模式等场景,但不保证原子性,复合操作需依赖synchronized或Atomic类。

在Java中,
volatile关键字主要用来保证多线程环境下共享变量的可见性。这意味着当一个线程修改了
volatile变量的值,这个新值会立即被刷新到主内存中,并且其他线程在读取该变量时,会强制从主内存中获取最新值,而不是使用它们各自工作内存中的旧缓存。它有效解决了线程间内存同步的问题,确保了数据的一致性。
解决方案
要使用
volatile关键字,你只需要在声明一个共享变量时加上它。它的作用机制比看起来要复杂一些,不仅仅是“不缓存”那么简单。本质上,
volatile会在读写操作前后插入特定的内存屏障(Memory Barrier)。
当一个线程写入一个
volatile变量时,它会强制将所有之前对该线程的写入操作刷新到主内存,并且阻止该写入操作与后续的读写操作重排序。 当一个线程读取一个
volatile变量时,它会强制使该线程的工作内存中的所有变量缓存失效,并从主内存中重新读取该
volatile变量的最新值,同时阻止该读取操作与之前或之后的任何操作重排序。
我们来看一个简单的例子。假设我们有一个标志位,用来通知另一个线程停止工作:
public class Worker {
// 没有volatile,stopRequested可能被线程缓存,导致线程无法及时停止
// private boolean stopRequested;
// 加上volatile,确保stopRequested的修改对所有线程立即可见
private volatile boolean stopRequested;
public void requestStop() {
stopRequested = true;
}
public boolean isStopRequested() {
return stopRequested;
}
public void run() {
while (!stopRequested) {
// 模拟工作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Worker is running...");
}
System.out.println("Worker stopped.");
}
public static void main(String[] args) throws InterruptedException {
Worker worker = new Worker();
Thread workerThread = new Thread(worker::run);
workerThread.start();
Thread.sleep(1000); // 让worker跑一会儿
System.out.println("Main thread requesting stop...");
worker.requestStop(); // 主线程修改stopRequested
workerThread.join(); // 等待worker线程结束
System.out.println("Main thread finished.");
}
}在这个
Worker例子中,如果
stopRequested没有被
volatile修饰,
run方法中的
while (!stopRequested)循环可能会因为
stopRequested变量被缓存到 CPU 寄存器或线程的本地工作内存中而一直看不到
main线程对其的修改,导致
Worker线程无法停止。加上
volatile后,
main线程对
stopRequested的写入操作会立即刷新到主内存,并且
Worker线程在每次循环读取
stopRequested时都会强制从主内存获取最新值,从而及时响应停止请求。
立即学习“Java免费学习笔记(深入)”;
volatile
关键字能保证原子性吗?
这是一个非常常见的误解。
volatile关键字不能保证操作的原子性。它只保证可见性和禁止指令重排序。原子性是指一个操作是不可中断的,要么全部执行成功,要么全部不执行,中间不会被其他线程打断。
考虑一个简单的自增操作:
count++。这个操作在底层实际上包含三个步骤:
- 读取
count
的当前值。 - 将
count
的值加 1。 - 将新值写回
count
。
如果
count被
volatile修饰,它确实保证了每次读取都是最新值,并且写入会立即刷新。但问题在于,这三个步骤不是一个原子操作。假设
count的初始值是 0:
- 线程 A 读取
count
(0)。 - 线程 B 读取
count
(0)。 - 线程 A 将
count
加 1 (1)。 - 线程 A 将 1 写回
count
(此时主内存中count
为 1)。 - 线程 B 将
count
加 1 (1)。 - 线程 B 将 1 写回
count
(此时主内存中count
仍然为 1)。
最终结果是 1,而不是我们期望的 2。尽管
volatile保证了可见性,但它无法阻止多个线程同时执行这三个步骤,从而导致数据丢失。
如果你需要保证原子性,你需要使用
java.util.concurrent.atomic包下的原子类(如
AtomicInteger、
AtomicLong等),或者使用
synchronized关键字或
Lock机制。原子类内部使用了 CAS(Compare-And-Swap)操作来保证原子性。
volatile
的内存语义与 happens-before 关系
要理解
volatile的深层工作原理,我们需要稍微深入一下 Java 内存模型(JMM)以及 happens-before 关系。JMM 定义了程序中各个变量的访问规则,以及在并发环境下如何保证数据一致性。happens-before 关系是 JMM 中一个核心概念,它定义了两个操作之间的偏序关系,保证了操作的可见性。如果操作 A happens-before 操作 B,那么 A 的结果对 B 是可见的,并且 A 的执行顺序在 B 之前。
volatile变量的读写操作具有特殊的 happens-before 语义:
-
volatile
写操作的 happens-before 语义: 对一个volatile
变量的写入操作,happens-before 任何后续对同一个volatile
变量的读取操作。这意味着,当一个线程写入volatile
变量时,所有在写入之前发生的动作(包括对非volatile
变量的修改)都会对后续读取该volatile
变量的线程可见。这就像一个屏障,将写入之前的操作都“推”到主内存。 -
volatile
读操作的 happens-before 语义: 对一个volatile
变量的读取操作,happens-before 任何后续对该变量的读写操作。更重要的是,在读取volatile
变量之后,该线程可以看到所有之前对该volatile
变量的写入操作,以及写入操作之前的所有操作。这就像一个屏障,确保了读取操作能够“拉取”到主内存的最新状态。
这些语义是通过内存屏障(Memory Barrier)来实现的。在 HotSpot JVM 中,
volatile变量的读写会插入以下内存屏障:
-
在
volatile
写操作前插入StoreStore
屏障: 保证在volatile
写之前,所有普通写操作都已刷新到主内存。 -
在
volatile
写操作后插入StoreLoad
屏障: 保证volatile
写操作对其他处理器可见。 -
在
volatile
读操作后插入LoadLoad
屏障: 保证volatile
读操作之后,所有普通读操作都读取到最新值。 -
在
volatile
读操作后插入LoadStore
屏障: 保证volatile
读操作之后,所有普通写操作都在volatile
读之后发生。
这些屏障阻止了编译器和处理器对指令进行重排序,从而维护了
volatile变量的可见性语义。
使用 volatile
关键字的常见误区与最佳实践
尽管
volatile很有用,但它并非万能药,使用不当反而会引入新的问题或者达不到预期效果。
常见误区:
-
误以为
volatile
能替代synchronized
: 正如前面所说,volatile
不保证原子性。如果你需要对共享变量进行复合操作(如i++
),或者需要保护一段临界区代码,volatile
是不够的,必须使用synchronized
或Lock
。 -
滥用
volatile
: 并不是所有共享变量都需要volatile
。如果一个变量只在一个线程中修改,或者它的修改不需要立即被其他线程感知,那么使用volatile
反而会引入不必要的内存屏障开销。 -
对
volatile
变量的复合操作: 如果一个volatile
变量的更新依赖于其旧值(例如计数器、累加器),那么volatile
无法保证线程安全。这种情况下,应该考虑使用Atomic
类或者synchronized
。
最佳实践:
作为状态标志位:
volatile
非常适合用于布尔型标志位,用来控制线程的停止、状态切换等。例如,前面例子中的stopRequested
。-
作为单例模式中的 DCL(Double-Checked Locking)屏障: 在实现线程安全的单例模式时,如果使用 DCL,必须将单例对象声明为
volatile
。这是为了防止指令重排序,确保当一个线程看到instance
不为null
时,它引用的对象是完全构造好的,而不是一个只分配了内存但尚未初始化的半成品。public class Singleton { private volatile static Singleton instance; // 必须是volatile private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); // 这步操作可能被重排序 } } } return instance; } }instance = new Singleton()
这行代码在 JVM 中大致会分为三步: a. 分配内存空间。 b. 初始化对象。 c. 将instance
引用指向分配的内存空间。 如果instance
没有volatile
修饰,JVM 可能会对这三步进行重排序,例如先执行 c 再执行 b。这时,如果线程 A 执行到 c 后,instance
已经不为null
,但对象可能尚未完全初始化。如果线程 B 此时进来,看到instance
不为null
,直接返回,就可能得到一个未完全初始化的对象,导致运行时错误。volatile
关键字在这里的作用就是禁止这种重排序。 有限状态机: 当一个变量在多个线程之间传递,并且它的值代表了某个有限状态机中的状态时,
volatile
可以确保状态的正确可见性。
总的来说,
volatile是 Java 并发编程中一个强大但需要谨慎使用的工具。它专注于解决可见性问题,通过内存屏障和 happens-before 语义来确保共享变量的最新值对所有线程可见,但它不能替代
synchronized或
Atomic类来解决原子性问题。理解它的工作原理和适用场景,能帮助我们写出更健壮、更高效的并发代码。










