一、volatile简介
前文中提到过JMM共享变量可见性问题,volatile就是为了解决这一问题。被volatile修饰的变量能够保证每个线程获取该变量的最新值,避免了可见性问题造成的数据脏读。
二、volatile原理
被volatile修改时的变量生成汇编代码进行写操作时会多出Lock前缀的指令。Lock指令主要有两方面的影响:
- 将当前处理器缓存行的数据写回系统内存
- 这个写回内存的操作会使的其它CPU里缓存的该内存地址的数据失效
为了提高处理器处理速度,处理器不直接和内存通信,而是先将系统内存的数据读到内部缓存(L1、L2或其它)后再进行操作。但操作完不知何时才会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存(主内存)。但是就算写到主内存,如果其它处理器缓存的值还是旧的,再执行计算操作就会有问题。
所以多处理器下,为了保证各个处理器缓存一致性,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是否过期。当处理器发现自己缓存对应的内存地址被修改,将会将当前处理器缓存设置成失效状态,当处理器对这个数据进行修改操作时会重新从系统内存中把数据读到处理器缓存中。
- Lock前缀的指令会引起处理器缓存写回内存
- 处理器缓存会写到主内存会导致其它处理器该数据缓存失效
- 处理器发现本地缓存失效后,再次从主内存中读取变量数据,获取到最新值
三、volatile内存语义实现
前文提到,处理器为了提高系统并发性,JMM在不改变正确语义的前提下,会允许编译器和处理器指令序列进行重排序,那如果想阻止重排序要怎么办?
内存屏障
JMM内存屏障分类:
java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表:
"NO"表示禁止重排序。为了实现volatile内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM采取了保守策略:
- 在每个volatile写操作的前面插入一个StoreStore屏障;
- 在每个volatile写操作的后面插入一个StoreLoad屏障;
- 在每个volatile读操作的后面插入一个LoadLoad屏障;
- 在每个volatile读操作的后面插入一个LoadStore屏障。
四、总结
- volatile通过内存屏障指令(Load/Store)解决了可见性问题
- volatile通过内存屏障指令(StoreStore/StoreLoad/LoadLoad/LoadStore)禁止了处理器重排序,解决了有序性问题
参考: