volatile
talk is cheap, show the code
public class NotAtomicVolatile {
private volatile int a = 0;
public void increment() {
a++; // 非原子操作
}
public int getCounter() {
return a;
}
public static void main(String[] args) throws InterruptedException {
NotAtomicVolatile example = new NotAtomicVolatile();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
// expect 2000
System.out.println("Final a value: " + example.getCounter());
}
}
上面的示例期待最终的结果为 2000,但是实际上不一定能够得到 2000,大部分情况下结果会小于 2000
为什么?
volatile
关键字主要是为多线程场景服务的,如果我使用了它但是依然无法保证多线程运行的结果,那么我为什么需要它?
volatile 关键字的特性之一为:
保证内存可见性但是不保证操作的原子性
这个特性是导致出现预料之外结果的原因,那么到底什么意思?
内存可见性和原子性
要理解 volatile
,必须要了解 java 的内存模型(JMM)
java 内存模型 JMM 是一种抽象层模型,它屏蔽了虚拟机在不同平台的具体实现,所以理解这个抽象层就能理解 volatile
在 JMM 中,存在 主内存
和 线程本地缓存
两种重要概念
由于没有互斥属性,线程是同时执行的,但是 volatile
修饰的变量 JMM 会保证
- 读取时从主内存获取最新值
- 写入时直接更新到主内存中
保证了最新数据对所有线程可见,这就是其所指的 内存可见性
但是仅针对于 “单个操作”,例如 读/写。对于复合操作,虽然遵循
单个操作内存可见性
,但是无法保证结果准确,因为多个线程可能会交错执行复合操作
再来看最初的示例
public class NotAtomicVolatile {
private volatile int a = 0;
public void increment() {
a++; // 非原子操作
}
public int getCounter() {
return a;
}
public static void main(String[] args) throws InterruptedException {
NotAtomicVolatile example = new NotAtomicVolatile();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
// expect 2000
System.out.println("Final a value: " + example.getCounter());
}
}
每次自增操作实际上都有三步,这就是 volatile
无法控制的复合操作
- 从主内存获取 a 的值
- 值 +1
- 新的值写回主内存
在 JMM 中分析其行为
由于两个线程不互斥,同时运行,那么在每一次循环中都有可能出现
- 线程 A:读取 a,值为 0
- 线程 B:读取 a,值为 0(因为线程 A 还没来得及写回)
- 线程 A:将 0 加 1,得到 1
- 线程 B:将 0 加 1,得到 1
- 线程 A:将 1 写回 a
- 线程 B:将 1 写回 a
最终此次循环结束时 a 的值还是 1,如此往复,导致了最终的结果不会是 2000
这就是 volatile 保证内存可见性,不保证操作原子性
的含义,以及可能会导致的后果
如果能保证多个线程中不会存在这个复合操作,那么可以使用它,因为相对于锁的方式,它更加轻量化
具体场景比如 多线程中需要共享状态标志、一写多读场景、以及下面的双重检查单例模式
下面的示例同时也展现了 volatile 的另一个特性:防止指令重新排序
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
// 1.分配内存空间
// 2.在内存中初始化对象
// 3.将引用指向分配的内存
// volatile 防止 1 2 3 被重排序成 1 3 2
instance = new Singleton();
}
}
}
return instance;
}
}
评论区