volatile 在内存语义上有两个作用,一个作用是保证被 volatile 修饰的共享变量对每个线程都是可见的,当一个线程修改了被 volatile 修饰的共享变量之后,另一个线程能够立刻看到修改后的数据。另一个作用是禁止指令重排。
保证可见性原理
volatile 能够保证共享变量的可见性。
如果一个共享变量使用 volatile 修饰,则该共享变量所在的缓存行会被会被要求进行缓存一致性校验。当一个线程修改了 volatile 修饰的共享变量之后,修改后的共享变量的值会立刻刷新到主内存,其他线程每次都从主内存中读取 volatile 修饰的共享变量,这就保证了使用 volatile 修饰的共享变量对线程的可见性。
例如:在程序中使用 volatile 修饰了一个共享变量 count,如下所示:
volatile long count = 0;
此时,线程对这个变量的读写都必须经过主内存。volatile 保证可见性的原理如图:
保证有序性核心原理
volatile 能够禁止指令重排,从而能够避免在高并发的环境下多个线程之间出现乱序执行的情况。volatile 禁止指令重排是通过内存屏障实现的,内存屏障本质上就是一条 CPU 指令,这个 CPU 指令有两个作用,一个是保证共享变量的可见性,另一个是保证指令的执行顺序。volatile 禁止指令重排的规则如表:
是否可以重排序 | 第二个操作 | ||
---|---|---|---|
第一个操作 | 普通读或写 | volatile 读 | volatile 写 |
普通写或读 | 可以重排序 | 可以重排序 | 不能重排序 |
volatile 读 | 不能重排序 | 不能重排序 | 不能重排序 |
volatile 写 | 可以重排序 | 不能重排序 | 不能重排序 |
为了实现上图的禁止指令重排的规则,JVM 编译器可以通过在程序编译生成的指令序列中插入「内存屏障」来禁止在内存屏障前后的指令发生重排。Java 内存模型建议 JVM 采用保守的策略严格禁止指令重排,volatile 读策略如图(1)所示:
- 在每个 volatile 读操作的后面都加入一个 LoadLoad 屏障,禁止后面的普通读与前面的 volatile 读发生指令重排。
- 在每个 volatile 读操作的后面都加入一个 LoadStore 屏障,禁止后面的普通写与前面的 volatile 读发生指令重排。
volatile 写策略如图(2)所示:
- 在每个 volatile 写操作的前面都加入一个 StoreStore 屏障,禁止前面的普通写与后面的 volatile 写发生指令重排。
- 在每个 volatile 写操作的后面都加入一个 StoreLoad 屏障,禁止前面的 volatile 写与后面的 volatile 读发生指令重排。
这种保守的内存屏障可以保证在任意 CPU 中都能够得到正确的执行结果。
Volatile 的局限性
volatile 虽然能够保证数据的可见性和有序性,但是无法保证数据的原子性。例如,下列代码中同时有两个线程对 volatile 修饰的 Long 类型的 count 值进行累加操作,count 的初始值为 0 ,每个线程都对 count 的值累加 1000 次,代码如下。
public class VolatileAtomicityTest {
private volatile Long count = 0L;
public void incrementCount(){
count++;
}
public Long execute() throws InterruptedException {
Thread thread1 = new Thread(()->{
IntStream.range(0, 1000).forEach((i) -> incrementCount());
});
Thread thread2 = new Thread(()->{
IntStream.range(0, 1000).forEach((i) -> incrementCount());
});
//启动线程1和线程2
thread1.start();
thread2.start();
//等待线程1和线程2执行完毕
thread1.join();
thread2.join();
//返回count的值
return count;
}
public static void main(String[] args) throws InterruptedException {
VolatileAtomicityTest multiThreadAtomicity = new VolatileAtomicityTest();
Long count = multiThreadAtomicity.execute();
System.out.println(count);
}
}
运行结果参考如下:
1448