Java中的volatile关键字经典应用场景和常见问题
作者:身如柳絮随风扬
这篇文章主要介绍了Java中volatile关键字经典应用场景和常见问题的相关资料,volatile是一种轻量级锁的实现,它针对的仅仅是共享变量,不会对线程加锁,更不会造成线程的阻塞,需要的朋友可以参考下
一、概述
volatile 是 Java 中用于修饰变量的关键字,它提供了一种轻量级的线程间通信机制。与 synchronized 相比,volatile 不会引起线程上下文切换和调度,因此性能开销更小。然而,其同步能力有限,使用不当容易产生线程安全问题。
二、核心特性
1.可见性保证(Visibility)
- 当某个线程修改
volatile变量的值时,该值会立即被强制刷新到主内存 - 其他线程读取该变量时,会从主内存重新加载最新值,而非使用本地线程缓存
- 解决了多线程环境下因 CPU 缓存导致的数据不一致问题
2.有序性保证(Ordering)
- 禁止编译器和处理器对
volatile变量的读写操作进行指令重排序 - 确保:
volatile写操作之前的任何读写操作不会被重排序到写之后volatile读操作之后的任何读写操作不会被重排序到读之前
- 建立 happens-before 关系,确保多线程间的操作顺序可见性
3.不保证原子性(Non-Atomicity)
volatile无法保证复合操作的原子性(如i++)- 复合操作由多个步骤组成,中间可能被其他线程中断
三、内存语义
volatile 写操作的内存屏障
[普通写/读操作] → [StoreStore屏障] → [volatile写] → [StoreLoad屏障]
volatile 读操作的内存屏障
[volatile读] → [LoadLoad屏障] → [LoadStore屏障] → [普通写/读操作]
内存屏障的作用:
- StoreStore屏障:确保 volatile 写之前的普通写操作已刷新到主内存
- StoreLoad屏障:确保 volatile 写完成后,后续的读操作能看到所有之前的写入
- LoadLoad屏障:确保 volatile 读之后的操作不会被重排序到读之前
- LoadStore屏障:确保 volatile 读之后的写操作不会被重排序到读之前
四、经典应用场景
1.状态标志(最常用)
public class ShutdownHandler {
private volatile boolean shutdownRequested = false;
public void shutdown() {
shutdownRequested = true;
}
public void doWork() {
while (!shutdownRequested) {
// 执行任务
}
}
}
2.双重检查锁定单例模式(DCL)
public class Singleton {
// 必须使用 volatile 防止指令重排序
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查:避免不必要的同步
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查:确保单例
// 创建对象分为三步(无volatile可能重排序):
// 1. 分配内存空间
// 2. 初始化对象
// 3. 将引用指向内存地址
// volatile 确保 2 在 3 之前完成
instance = new Singleton();
}
}
}
return instance;
}
}
3.一次性安全发布(One-Time Safe Publication)
public class Resource {
private volatile Resource resource;
public Resource getResource() {
if (resource == null) {
synchronized (this) {
if (resource == null) {
resource = new Resource();
}
}
}
return resource;
}
}
4.独立观察(Independent Observation)
public class SensorReader {
private volatile double currentTemperature;
// 一个线程定期更新温度
public void updateTemperature(double temp) {
currentTemperature = temp;
}
// 多个线程同时读取最新的温度值
public double getTemperature() {
return currentTemperature;
}
}
五、volatile与synchronized详细对比
| 特性 | volatile | synchronized |
|---|---|---|
| 原子性 | 仅保证单个读/写操作的原子性 不保证复合操作(如 i++)的原子性 | 保证整个代码块/方法的原子性 |
| 可见性 | 保证变量对所有线程立即可见 | 保证变量对所有线程可见 |
| 有序性 | 禁止指令重排序 (通过内存屏障实现) | 保证有序性 (但允许同步块内重排序) |
| 阻塞性 | 非阻塞机制 线程不会挂起 | 阻塞机制 获取不到锁的线程会挂起等待 |
| 性能 | 轻量级,性能开销小 (仅内存屏障开销) | 重量级,性能开销较大 (涉及锁竞争、上下文切换) |
| 作用范围 | 变量级别 | 代码块或方法级别 |
| 适用场景 | 状态标志、一次性发布 | 复杂同步逻辑、需要原子性的复合操作 |
六、常见误区与注意事项
错误示例:误以为 volatile 能保证原子性
public class Counter {
private volatile int count = 0;
// 线程不安全!count++ 不是原子操作
public void increment() {
count++; // 实际包含:读 → 加1 → 写 三个步骤
}
}
正确解决方案
// 方案1:使用 synchronized(适合复杂同步)
public class SynchronizedCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
// 方案2:使用原子类(推荐,性能更好)
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // CAS操作,保证原子性
}
public int getCount() {
return count.get();
}
}
// 方案3:使用 volatile + CAS(高级用法)
public class VolatileCASCounter {
private volatile int count = 0;
private static final sun.misc.Unsafe UNSAFE = // 获取Unsafe实例
public void increment() {
int current;
do {
current = count;
} while (!UNSAFE.compareAndSwapInt(this, OFFSET, current, current + 1));
}
}
使用建议总结
- 优先考虑不可变对象:避免使用
volatile,设计为不可变对象 - 一个写线程,多个读线程:典型的
volatile适用场景 - 变量不参与不变性约束:
volatile变量不应依赖于其他变量,也不应被其他变量依赖 - 替代方案优先:考虑使用
java.util.concurrent.atomic包下的原子类 - 谨慎使用:只在明确理解其语义的场景下使用
七、与 JMM(Java内存模型)的关系
volatile 变量的读写操作建立了 happens-before 关系:
- 对
volatile变量的写操作 happens-before 于后续对该变量的读操作 - 这与
synchronized的释放锁 happens-before 于获取锁的语义类似
八、性能考量
性能测试对比
// volatile 变量访问 vs 普通变量访问 volatile int vCounter = 0; int counter = 0; // volatile 访问有约10-20%的性能损耗 // 但相比 synchronized 的数千倍性能损耗可忽略不计
最佳实践
- 避免过度使用:不必要的
volatile修饰会增加内存屏障开销 - 结合使用:
volatile适合与final结合,用于安全发布 - 替代方案:考虑使用
ThreadLocal避免共享变量
九、扩展知识:volatile 在 JSR-133 中的增强
在 Java 5 之前,volatile 只保证可见性,不保证有序性。JSR-133(Java内存模型修订)强化了 volatile 的语义,通过内存屏障同时保证了可见性和有序性。
十、总结
volatile 是 Java 并发编程中的重要工具,但理解其局限性至关重要:
适合使用 volatile 的场景:
- 状态标志位(一个线程写,多个线程读)
- 一次性安全发布(如单例模式的DCL实现)
- 独立观察(如传感器数据读取)
不适合使用 volatile 的场景:
- 需要原子性的复合操作
- 变量参与不变性约束
- 依赖其他变量的计算
黄金法则: 当您不确定是否需要使用 volatile 时,优先考虑使用更高层次的并发工具(如 java.util.concurrent 包中的类),这些工具通常封装了更安全、更高效的并发实现。
到此这篇关于Java中volatile关键字经典应用场景和常见问题的文章就介绍到这了,更多相关Java中volatile关键字内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
