参考:https://segmentfault.com/a/1190000037486483
对于Java程序员来说,volatile关键字很重要,即使我们在工作中没有直接使用到volatile,但是如果使用过:ConcurrentHashMap、AtomicInteger、FutureTask、ThreadPoolExecutor等功能,它们的底层都使用了volatile关键字。下面来看一下volatile的底层原理。
在介绍volatile底层原理之前,让我们先看看什么是JMM(即java内存模型)。
java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝。工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。
Java内存模型会带来三个问题:
1、可见性问题
线程A和线程B同时操作共享数据C,线程A修改的结果,线程B是不知道的,即不可见的。
2、竞争问题
刚开始数据C的值为1,线程A和线程B同时执行加1操作,正常情况下数据C应该为3,但是在并发的情况下,数据C却还是2
3、重排序问题
JVM为了优化指令的执行效率,会对代码指令进行重排序。
volatile的底层原理
java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序,从而让程序按我们预想的流程去执行。
java的内存屏障指令如下:
屏障类型:
LoadLoad
指令示例:
Load1
LoadLoad
Load2
说明:保证Load1的读操作优先于Load2执行
StoreStore
指令示例:
Store1
StoreStore
Store2
说明:
保证Store1的写操作优先于Store执行,并刷新主内存
LoadStore
指令示例:
Load1
LoadStore
Store2
保证Load1的读操作优先于Store2的写操作执行
StoreLoad
指令示例:
Store1
StoreLoad
Load2
保证Store1的写操作已刷新到主内存之后,Load2及后面的读操作才能执行
对于volatile的写操作,在其前后分别加上 StoreStore 和 StoreLoad指令
对于volatile的读操作,在其后加上 LoadLoad 和 LoadStore指令
由上图可以看到,内存屏障是可以保证volatile变量前后读写顺序的。
此外,对volatile变量写操作时,使用store指令会强制线程刷新数据到主内存,读操作使用load指令会强制从主内存读取变量值。
上面我们提到过:
“
2、竞争问题
刚开始数据C的值为1,线程A和线程B同时执行加1操作,正常情况下数据C应该为3,但是在并发的情况下,数据C却还是2
”
volatile关键字可以解决可见性和重排序问题。但是不能解决竞争问题,无法保证操作的原子性,解决竞争问题需要加锁,或者使用cas等无锁技术。
总结
volatile的底层是通过:store,load等内存屏障命令,解决JMM的可见性和重排序问题的。但是它无法解决竞争问题,要解决竞争问题需要加锁,或使用cas等无锁技术。
使用volatile保证线程间的可见性和重排序问题,相对于synchronized等加锁机制更轻量级。