Java volatile 的作用和局限详解以及为什么不能保证 i++ 的原子性
作者:矜予
volatile 关键字详解
在 Java 中,volatile 主要解决的是可见性和有序性问题,但不能解决所有线程安全问题,尤其不能保证复合操作的原子性。
1. volatile 的作用
1.1 保证可见性
如果一个变量被声明为 volatile,那么一个线程对它的修改,其他线程可以立即看到。
例如:
volatile boolean running = true;
Thread t1 = new Thread(() -> {
while (running) {
// do something
}
});
Thread t2 = new Thread(() -> {
running = false;
});如果 running 不是 volatile,线程 t1 可能一直读取自己工作内存中的旧值,看不到 t2 修改后的 false。
加上 volatile 后,t2 修改 running 后,t1 能够及时看到最新值。
1.2 禁止指令重排序
volatile 还可以禁止特定场景下的指令重排序。
例如经典的双重检查锁单例:
class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}为什么 instance 要用 volatile?
因为:
instance = new Singleton();
这个操作大致可以分为三步:
1. 分配内存 2. 初始化对象 3. 将 instance 指向这块内存
如果没有 volatile,步骤 2 和 3 可能发生重排序:
1. 分配内存 3. 将 instance 指向这块内存 2. 初始化对象
这样其他线程可能看到 instance != null,但拿到的是一个还没有初始化完成的对象。
使用 volatile 可以禁止这种重排序。
2. volatile 的局限
volatile 不能保证复合操作的原子性。
它能保证的是:
volatile int i; i = 10; int j = i;
这种单次读、单次写的可见性和有序性。
但它不能保证下面这种操作是线程安全的:
volatile int i = 0; i++;
3. 为什么 volatile 不能保证 i++ 的原子性?
因为 i++ 不是一个单独的操作,而是一个复合操作。
它大致包含三步:
1. 读取 i 的值 2. 对 i 加 1 3. 把新值写回 i
例如:
i++;
等价于:
int temp = i; temp = temp + 1; i = temp;
即使 i 是 volatile,也只能保证每次读取的是最新值、每次写入能被其他线程看到,但不能阻止多个线程同时执行这三步。
4. 举个例子
假设初始值:
i = 0
现在有两个线程同时执行:
i++;
可能发生如下过程:
线程 A 读取 i,得到 0 线程 B 读取 i,得到 0 线程 A 计算 0 + 1,得到 1 线程 B 计算 0 + 1,得到 1 线程 A 写回 i = 1 线程 B 写回 i = 1
最终结果是:
i = 1
但我们期望两个线程各加一次,结果应该是:
i = 2
这就是典型的丢失更新问题。
volatile 不能避免这种情况,因为它没有加锁,也没有 CAS 机制,不能保证“读取-修改-写入”这一整个过程不可被打断。
5. 如何保证 i++ 的原子性?
方式一:使用 synchronized
private int i = 0;
public synchronized void increment() {
i++;
}或者:
private int i = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
i++;
}
}synchronized 可以保证同一时刻只有一个线程执行临界区代码。
方式二:使用 AtomicInteger
import java.util.concurrent.atomic.AtomicInteger; AtomicInteger i = new AtomicInteger(0); i.incrementAndGet();
AtomicInteger 底层通常基于 CAS 实现,可以保证自增操作的原子性。
例如:
AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}方式三:使用 LongAdder
在高并发计数场景下,可以使用:
import java.util.concurrent.atomic.LongAdder; LongAdder adder = new LongAdder(); adder.increment();
LongAdder 在高并发下通常比 AtomicInteger 或 AtomicLong 性能更好。
6. 总结
volatile 的作用:
1. 保证变量修改对其他线程立即可见 2. 禁止相关指令重排序
volatile 的局限:
1. 不能保证复合操作的原子性 2. 不能替代 synchronized 3. 不能解决竞态条件问题
i++ 不能通过 volatile 保证原子性,因为它实际上是:
读取 i 计算 i + 1 写回 i
多个线程同时执行时,可能发生丢失更新。
如果要保证 i++ 线程安全,应使用:
synchronized Lock AtomicInteger LongAdder
简单来说:
volatile保证“看得见”,但不保证“抢得住”。i++需要的是原子性的“读-改-写”,而volatile只能保证单次读写的可见性。
到此这篇关于volatile 的作用和局限是什么?为什么不能保证 i++ 的原子性?的文章就介绍到这了,更多相关volatile 的作用和局限内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
