深入理解关键字Volatile!
深入理解关键字Volatile!
月伴飞鱼相比于 synchronized
关键字(重量级锁)对性能影响较大。
使用 volatile
不会引起上下文的切换和调度,所以 volatile
对性能的影响较小,开销较低。
volatile
可以保证其修饰的变量的可见性和有序性。无法保证原子性(不能保证完全的原子性,只能保证单次读/写操作具有原子性,即无法保证复合操作的原子性)。
volatile
如何实现可见性?
被
volatile
修饰的共享变量flag
被一个线程修改后。JMM(Java内存模型)会把该线程的CPU内存中的共享变量
flag
立即强制刷新回主存中。并且让其他线程的CPU内存中的共享变量
flag
缓存失效。这样当其他线程需要访问该共享变量
flag
时,就会从主存获取最新的数据。
volatile
实现可见性的原理
Lock指令(汇编指令):
被
volatile
修饰的变量会多一个lock前缀的指令。会将处理器缓存的数据写回主存中,同时使其他线程的处理器缓存的数据失效。
这样其他线程需要使用数据时,会从主存中读取最新的数据,从而实现可见性。
内存屏障(CPU指令):
为了性能优化,JMM 在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序。
JMM 提供了内存屏障阻止这种重排序。
Store屏障:
- 当一个线程修改了volatile变量的值,它会在修改后插入一个写屏障。
- 告诉处理器在写屏障之前将所有存储在缓存中的数据同步到主内存。
Load屏障:
- 当另一个线程读取volatile变量的值,它会在读取前插入一个读屏障。
- 告诉处理器在读屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果。
如果被 volatile 修饰时会多一个
ACC_VOLATILE
,JVM把字节码生成机器码时会在相应位置插入内存屏障指令。因此可以通过读写屏障实现
volatile
修饰变量的可见性。
volatile
如何实现有序性?
volatile
保证变量有序性,禁止指令重排序。
volatile
实现有序性的原理
Java编译器会在生成指令时在适当位置插入内存屏障来禁止特定类型的处理器重排序。
内存屏障中禁止指令重排序的内存屏障的四种指令:
指令 | 说明 |
---|---|
LoadLoad 屏障 | 保证在该屏障之后的读操作,不会被重排序到该屏障之前的读操作 |
StoreStore屏障 | 保证在该屏障之后的写操作,不会被重排序到该屏障之前的写操作,并且该屏障之前的写操作已被刷入主存 |
StoreLoad 屏障 | 保证在该屏障之后的读操作,能够看到该屏障之前的写操作对应变量的最新值 |
LoadStore 屏障 | 保证在该屏障之后的写操作,不会被重排序到该屏障之前的读操作 |
volatile的插入屏障策略
- 在每个
volatile
写操作的前面插入一个 StoreStore 屏障 - 在每个
volatile
写操作的后面插入一个 StoreLoad 屏障 - 在每个
volatile
读操作的后面插入一个 LoadLoad 屏障 - 在每个
volatile
读操作的后面插入一个 LoadStore 屏障
即在每个volatile写操作前后分别插入内存屏障,在每个volatile读操作后插入两个内存屏障。
volatile
为什么不能保证原子性?
volatile
无法保证复合操作的原子性,但能保证单个操作的原子性。
volatile
常见的应用场景?
状态标志位:
使用
volatile
修饰一个变量通过赋值不同的常数或值来标识不同的状态。
1 | /** |
双重检查DLC:
单例模式的双重检查DLC可以通过
volatile
来修饰从存储单例模式对象的变量。