【并发编程】详解volatile
【并发编程】
- 一、volatile是什么?
- 二、volatile的三大核心特性
- 1. 可见性
- 2. 顺序性
- 3. 不保证原子性
- 三、volatile的底层实现原理
- 1. 内存屏障的作用
- 2. volatile变量的内存屏障插入规则
- 四、volatile和synchronized的核心区别
- 五、volatile的典型使用场景
- 1. 状态标志位
- 2. 双重检查锁的单例模式
- 3. 轻量级的共享变量传递
- 扩展:volatile和AtomicInteger
volatile是什么?它能解决什么问题?它的底层实现原理是什么?和synchronized有啥区别?哪些场景适合用volatile?
一、volatile是什么?
在Java并发编程里,多线程操作共享变量时,经常会出现“线程A改了变量,线程B却看不到”的情况。这时候,volatile就能派上用场——它是Java提供的轻量级同步关键字,专门用来保证共享变量的“可见性”,同时还能禁止“指令重排序”。
简单说,当一个变量被volatile修饰后,它就有了两个核心特性:
- 线程对变量的修改会立刻同步到主内存,不会只停留在自己的工作内存里;
- 其他线程读取这个变量时,会直接从主内存加载最新值,而不是用自己工作内存里的旧数据。
举个例子:没有volatile修饰时,线程1把flag
改成true
,线程2可能一直读的是自己工作内存里的false
,导致循环无法结束;加了volatile后,线程1改完flag
会马上同步到主内存,线程2每次读flag
都会从主内存拿最新值,能立刻感知到变化。
// 没有volatile,线程2可能永远看不到flag的变化
private boolean flag = false;// 有volatile,线程2能实时感知flag的修改
private volatile boolean flag = false;
二、volatile的三大核心特性
1. 可见性
这是volatile最核心的作用。要理解可见性,得先知道Java的“内存模型”(【并发编程】彻底搞懂Java内存模型 JMM:从底层原理到并发问题解决):
- 每个线程都有自己的“工作内存”,操作变量时会先把主内存的变量拷贝到工作内存;
- 线程修改变量后,会先更新工作内存里的值,再“不定期”同步回主内存;
- 其他线程读变量时,也会“不定期”从主内存刷新工作内存里的副本。
这种“不定期”就导致了可见性问题。而volatile能强制线程做到两点:
- 修改变量后,立刻将工作内存的新值刷回主内存;
- 读取变量前,先将工作内存的旧值清空,再从主内存重新加载。
这样一来,所有线程操作的都是主内存的“最新值”,不会出现“一个改了、一个没看见”的情况。
2. 顺序性
编译器和CPU为了提高性能,会对代码执行顺序做“合理调整”——这就是指令重排序。平时单线程下没问题,但多线程下可能出大错。
比如单例模式的“双重检查锁”,没有volatile修饰instance
时,可能会出现“对象还没初始化完,就被其他线程拿走用”的情况:
// 有问题的双重检查锁(缺少volatile)
public class Singleton {private static Singleton instance; // 没有volatilepublic static Singleton getInstance() {if (instance == null) { // 第一次检查synchronized (Singleton.class) {if (instance == null) { // 第二次检查instance = new Singleton(); // 这里可能被重排序}}}return instance;}
}
new Singleton()
其实分三步:
- 分配内存空间;
- 初始化对象;
- 把
instance
指向内存空间。
编译器可能会把步骤2和3重排序成“1→3→2”。这时候线程A执行到instance = new Singleton()
,刚做完步骤3(instance
不为null),还没做步骤2(对象没初始化);线程B过来判断instance != null
,直接拿走没初始化的对象,一用就报错。
而volatile能禁止这种重排序,强制new
操作按“1→2→3”的顺序执行,避免上述问题。
3. 不保证原子性
这是volatile最容易被误解的点——很多人以为它能替代synchronized,解决所有并发问题,但其实它管不了“多个线程同时修改变量”的原子性问题。
比如两个线程同时执行count++
,即使count
被volatile修饰,最终结果也可能小于预期值。因为count++
不是“一步操作”,而是分三步:
- 从主内存读取
count
的最新值; - 在工作内存里把
count
加1; - 把新值刷回主内存。
这三步中间可能被其他线程打断。比如线程A读了count=10
,还没来得及加1,线程B就也读了count=10
;之后A加1变成11刷回主内存,B加1也变成11刷回主内存——原本该是12的结果,最终成了11。
所以,要解决原子性问题,还得用synchronized
、ReentrantLock
或者AtomicInteger
这类原子类。
三、volatile的底层实现原理
volatile的特性是依赖CPU的“内存屏障”(Memory Barrier)指令实现的。Java虚拟机在处理volatile变量时,会在生成的字节码里插入特定的内存屏障,从而约束编译器和CPU的行为。
1. 内存屏障的作用
内存屏障有两个核心功能:
- 禁止屏障两侧的指令重排序;
- 强制将工作内存的缓存数据刷回主内存(写屏障),或强制从主内存重新加载数据(读屏障)。
2. volatile变量的内存屏障插入规则
Java虚拟机会给volatile变量的读写操作加上不同的内存屏障,具体规则如下:
- 写操作后:插入“StoreStore屏障”和“StoreLoad屏障”。
- StoreStore屏障:保证在当前volatile变量写操作之前,所有普通变量的写操作都已经刷回主内存;
- StoreLoad屏障:保证当前volatile变量的写操作已经刷回主内存后,再执行后续的读操作。
- 读操作前:插入“LoadLoad屏障”和“LoadStore屏障”。
- LoadLoad屏障:保证在读取当前volatile变量之前,先清空工作内存的旧数据,从主内存加载最新值;
- LoadStore屏障:保证当前volatile变量的读操作完成后,再执行后续普通变量的写操作。
正是这些内存屏障,让volatile实现了“可见性”和“禁止重排序”的特性。
四、volatile和synchronized的核心区别
很多人会把volatile和synchronized(【并发编程】深入理解Synchronized:从并发问题到锁升级的完整解析)搞混,其实它们的定位和能力完全不同,核心区别有4点:
对比维度 | volatile | synchronized |
---|---|---|
作用层级 | 变量级(只修饰变量) | 代码块/方法级(修饰代码块或方法) |
原子性 | 不保证 | 保证(同一时间只有一个线程执行) |
可见性 | 保证(通过内存屏障) | 保证(释放锁时刷回主内存) |
有序性 | 保证(禁止重排序) | 保证(单线程执行,天然有序) |
性能 | 轻量级,几乎无开销 | 重量级,有锁竞争时会阻塞线程 |
简单地说:volatile是“轻量级同步手段”,只解决可见性和有序性,适合变量的“单次读/写”场景;synchronized是“重量级锁”,能解决原子性、可见性、有序性所有问题,但性能开销更大,适合复杂的同步逻辑。
五、volatile的典型使用场景
volatile不是“万能药”,但在某些场景下,它比synchronized更高效,是最佳选择。
1. 状态标志位
这是volatile最常用的场景——用一个volatile变量作为“线程间的信号开关”,控制线程的启动、停止或执行逻辑切换。
比如线程A负责循环执行任务,线程B通过修改volatile修饰的stopFlag
,让线程A停止循环:
public class VolatileDemo {// 用volatile修饰状态标志位private volatile boolean stopFlag = false;// 线程A:循环执行任务,直到stopFlag为truepublic void startTask() {new Thread(() -> {while (!stopFlag) {// 执行具体任务System.out.println("线程A正在执行任务...");}System.out.println("线程A停止执行");}).start();}// 线程B:修改stopFlag,让线程A停止public void stopTask() {stopFlag = true;}
}
这里用volatile正好合适:stopFlag
的操作是“单次写”(线程B)和“单次读”(线程A),没有原子性问题,用volatile保证可见性即可,性能比synchronized高。
2. 双重检查锁的单例模式
前面提到过,双重检查锁的单例模式里,instance
必须用volatile修饰,否则会因为指令重排序出现“对象未初始化完成就被使用”的问题。正确的写法如下:
public class Singleton {// 关键:用volatile修饰instanceprivate static volatile Singleton instance;private Singleton() {} // 私有构造,防止外部newpublic static Singleton getInstance() {if (instance == null) { // 第一次检查(无锁,提高效率)synchronized (Singleton.class) { // 加锁if (instance == null) { // 第二次检查(防止多线程重复创建)instance = new Singleton(); // 禁止重排序}}}return instance;}
}
3. 轻量级的共享变量传递
当多个线程需要共享一个“单次赋值、多次读取”的变量时,用volatile修饰可以保证所有线程读到的都是最新值。
比如线程A加载配置文件后,把配置对象赋值给volatile修饰的config
变量;其他线程读取config
时,能立刻拿到最新的配置,不用加锁。
扩展:volatile和AtomicInteger
很多人会问:既然volatile不保证原子性,那AtomicInteger为什么能保证原子性?其实AtomicInteger的底层是“volatile+CAS(Compare And Swap)”的组合。
- AtomicInteger的value变量被volatile修饰,保证可见性;
- 它的
incrementAndGet()
等方法,通过CPU的CAS指令实现“原子性修改”——CAS会先比较value的当前值和预期值,如果一致才修改,不一致就重试,直到成功。
简单说:volatile负责“看见最新值”,CAS负责“修改时不被打断”,二者结合才实现了原子性。而volatile单独使用时,没有CAS的“比较-修改”逻辑,所以解决不了原子性问题。