一、volatile的作用
-
可见性:当一个变量被volatile修饰时,将保证此变量对所有线程的可见性。
-
有序性:使用volatile修饰的变量能禁止指令重排优化。
二、禁止指令重排
在底层源码中,volatile是通过给变量赋值之后,加一个"内存屏障"来实现禁止指令重排。
2.1 内存屏障是什么?
内存屏障就是一类同步屏障指令,是CPU或者编译器在对内存随机访问的操作中的一个同步点,只有在此点之前的所有读写操作都执行后才可以执行此点之后的操作。
2.2 JMM层面的“内存屏障”
先了解两个指令。
- load(加载):将主存中读取到的值指向工作内存中变量副本。
- store(存储):将工作内存中变量副本的值传递到主内存。
屏障名称 | 示例 | 说明 |
---|---|---|
LoadLoad Barriers | load1;LoadLoad;load2; | 确保load1读操作先于load2 |
StoreStore Barriers | store1; StoreStore; store2 | 确保store1将数据写回主存的操作先于store2 |
LoadStore Barriers | load1; LoadStore; store2 | 确保load1读操作先于store2将数据写回主存的操作 |
StoreLoad Barriers | store1; LoadStore; load2 | 确保store1将数据写回主存的操作先于load2的操作,它会使该屏障之前所有内存访问指令完成之后,才能执行该屏障之后的内存访问指令。 |
StoreLoad Barriers同时具备其他三个屏障的效果,开销比较大,被目前多数cpu所支持。
三、源码剖析
- Java代码
public class Demo2 {
volatile static int num = 0;
public static void main(String[] args) {
num = 8;
}
}
- 字节码文件
用javap -v 命令查看生成的class文件。
字节码层面num属性的访问标志是ACC_STATIC, ACC_VOLATILE。
putstatic:给静态变量赋值的操作。
- JVM源码
我们直接看openjdk8底层的putstatic赋值源码。openjdk8源码git地址
/hotspot/src/share/vm/interpreter路径下的bytecodeInterpreter.cpp文件
3.1、cache->is_volatile():根据num属性访问标识判断是否是volatile;
bool is_volatile () const { return (_flags & JVM_ACC_VOLATILE ) != 0; }
如果是volatile根据不同类型赋值,执行OrderAccess::storeload();
,这个方法的名字比较眼熟,storeload不就是内存屏障的一种嘛,见名思意,这个代码中应该是和内存屏障有关的代码。
bytecodeInterpreter.cpp:
3.2、OrderAccess::storeload()
这里有一个需要注意的地方,因为不同的操作系统和不同的CPU有不同的实现,这里记得要选择对应的文件查看。
咱们选择Linux_x86版本的查看。
src/os_cpu/linux_x86/vm/orderAccess_linux_x86.inline.hpp:
从上图代码中,可以看出最终会执行OrderAccess::fence(),里面会做一些判断,但最终会执行一个lock前缀指令。
lock指令是一个汇编指令,在执行它时,会使CPU释放一个LOCK# 信号,它会锁住对应变量的内存地址,这样就确保在多处理器或多线程的情况下互斥使用这个内存地址,相当于当前线程给这个变量赋值时,其他线程都不能读取。当指令执行完毕,这个锁定动作也就会消失。
总结
底层在给volatile变量赋值时,会发送一个lock指令,这个指令会锁住特定的内存地址,并将线程中该变量缓存行的数据立即写回主存,并触发总线嗅探机制。读取和修改都会经过总线,总线嗅探机制会嗅探总线上共享变量的改变,如果CPU发现自己线程中缓存的该共享变量被修改了,会将该缓存行置为无效状态,然后从主存中再次加载该共享变量的值。