你好,我是吴计可师,一个工作十多年的后端开发,曾就职京东、阿里等多家互联网头部企业。
文章可能会比较长,主要解析的非常详解,或涉及一些底层知识,供面试高阶难度用。可以根据自己实际理解情况合理取舍阅读
volatile 是 Java 中的一个关键字,用来修饰变量,保证线程间的可见性和一定的有序性。
可见性:一个线程修改了 volatile 变量的值,新值对其他线程立即可见。
禁止指令重排序:对 volatile 变量的读写操作不能被重排序。
不保证原子性:volatile 仅保证读写的可见性,不保证复合操作(如 i++)的原子性。
volatile 的实现主要依赖于 CPU 的 缓存一致性协议(MESI) 和 Java 内存模型(JMM)的实现。其核心机制如下:
当一个线程对 volatile 变量进行写操作时,CPU 会将该变量所在的缓存行标记为无效,从而强制其他线程从主内存重新读取变量值。
内存屏障:编译器会在 volatile 变量的读写操作前后插入特定的指令,用来限制指令重排序并实现内存可见性。
Java 编译器会在 volatile 变量的操作前后插入以下两种内存屏障:
Store屏障(写屏障):用于确保在写入 volatile 变量之前,所有的普通写操作都已经同步到主内存中。
Load屏障(读屏障):用于确保在读取 volatile 变量之后,所有的普通读操作都从主内存中获取最新值。
volatile int a = 1;
a = 2;
编译器会生成:
写屏障:确保之前的写操作完成后再更新 a 的值。
将 a = 2 的值刷新到主内存。
int b = a;
编译器会生成:读屏障:确保在读取 a 之前重新从主内存中加载。
在底层,volatile 的实现通常通过以下机制:
强制当前 CPU 缓存行写入主内存。
使其他 CPU 的缓存行无效,确保数据一致性。
lock addl $0x0, (%esp)
此指令会强制执行内存屏障,防止指令重排序,并刷新主存数据。
volatile int count = 0;
public void increment() {
count++; // 不是线程安全的
}
count++ 实际上由三步组成:
读取 count 的值。
对值加 1。
将结果写回到 count。
由于 volatile 不保证这三步的原子性,可能会出现多个线程竞争导致最终结果不正确。为此,需要使用原子类或同步机制(如 AtomicInteger 或 synchronized)。
状态标志:
volatile boolean stop = false;
多线程通过共享变量控制任务的停止。
单例模式的双重检查锁(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;
}
}
volatile 确保实例的初始化过程对所有线程可见,并防止指令重排序。
5.1 volatile 和 synchronized 的区别是什么?
特性 | volatile | synchronized |
---|---|---|
功能 | 保证变量的可见性、有序性 | 保证代码块的原子性和可见性 |
性能 | 较轻量,开销低 | 比较重,涉及线程的上下文切换 |
适用场景 | 单变量的读写 | 多线程协作的复杂场景 |
阻塞 | 非阻塞 | 阻塞 |
volatile 不能替代 synchronized 的主要原因:
volatile 不保证复合操作(如 i++)的原子性。
volatile 无法同步复杂逻辑(如代码块中的多变量操作)。
volatile 仅用于标识简单的状态标志或变量共享,而 synchronized 可以保护一段代码块的线程安全。
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 步在第 2 步之前完成,从而使其他线程访问到尚未初始化的对象。
使用 volatile 变量作为标志位,保证线程之间的可见性:
class StopThread implements Runnable {
private volatile boolean stop = false;
public void run() {
while (!stop) {
// 执行业务逻辑
}
}
public void stop() {
stop = true;
}
}
volatile 确保 stop 的修改对其他线程可见,安全地终止线程。
状态标志:如停止线程的标志。
单例模式的双重检查锁。
轻量级的读多写少场景。
写 volatile 变量时:
线程会将该变量值刷新到主内存。
之前的写操作对其他线程可见。
读 volatile 变量时:
线程会从主内存读取最新值。
后续的读操作能够看到最新值。
内存屏障在底层通过 lock 指令 实现,防止 CPU 指令重排序。
class Test {
private volatile boolean flag = false;
private int value = 0;
public void writer() {
value = 42; // 1
flag = true; // 2
}
public void reader() {
if (flag) { // 3
System.out.println(value); // 一定是 42
}
}
}
因为 flag 是 volatile 修饰,语义上保证 1 的操作一定先于 2。在 reader 中如果看到 flag 为 true,则一定能读取到最新的 value。
今天的内容就分享到这儿,喜欢的朋友可以关注,点赞。有什么不足的地方欢迎留言指出,您的关注是我前进的动力!