volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
- 对 volatile 变量的写指令后会加入写屏障
- 对 volatile 变量的读指令前会加入读屏障
1. 如何保证可见性
写屏障(sfence,Store Fence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
而读屏障(lfence,Load Fence)保证在该屏障之后的,对共享变量的读取,加载的是主存中最新数据
t1和t2两个线程执行actor2和actor1方法的时序图如下:
2. 如何保证有序性
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
t1和t2两个线程执行actor2和actor1方法的时序图:
volatile虽然保证了可见性和有序性,但仍无法解决指令交错的问题:
- 写屏障仅仅是保证写屏障之后其他线程的读能够读到最新的结果,但不能保证读跑到它前面去
- 而有序性的保证也只是保证了本线程内相关代码不被重排序(其他线程的代码可能会穿插该线程的代码)
如下图,t2线程的读跑在t1线程的写屏障前面去了:
3. double-checked locking 问题
以著名的 double-checked locking 单例模式为例:
以上的实现特点是:
- 懒惰实例化
- 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
- 有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外
但在多线程环境下,上面的代码是有问题的,getInstance 方法对应的字节码见下图:
其中红框处对应getInstance方法中的“INSTANCE = new Singleton()”代码
- 17 表示创建对象,将对象引用入栈 //new Singleton()
- 20 表示复制一份对象引用 //对象引用的地址
- 21 表示利用一个对象引用,调用构造方法
- 24 表示将一个对象引用,赋值给 static INSTANCE
其中17涉及类加载检查,分配内存,初始化零值和设置对象头,20负责复制对象引用,21用到20提供的对象引用,调用无参构造函数对该对象初始化。(初始化零值和无参构造函数的区别:初始化零值赋初值,无参构造函数内容可根据程序员需要自定义,可以赋程序员想要的值)
也许 jvm 会将上面字节码的执行顺序优化为:先执行 24,再执行 21。那么如果两个线程 t1,t2 按如下时间序列执行:
图中关键在于 “0: getstatic” 这行代码在 monitor 控制之外,可以越过 monitor 读取INSTANCE 变量的值。
这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例。
如何解决?对 INSTANCE 变量使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效。
4. double-checked locking 解决
如下图所示,给INSTANCE变量加了volatile来禁止指令重排序,解决前面3的dcl问题。
getInstance方法对应的字节码见下图,无法看出来volatile 指令禁止指令重排序的效果(但可通过后面的时序图看出):
如上面红框处的注释内容所示,读写 volatile 变量时会加入内存屏障(Memory Barrier(Memory Fence)),保证下面两点:
- 可见性
- 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中
- 而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据
- 有序性
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
- 更底层是读写变量时使用 lock addl指令(该指令在汇编语言中出现,会被解析为内存屏障。可见JVM系列之:从汇编角度分析Volatile-腾讯云开发者社区-腾讯云 (tencent.com))来保证多核 CPU (多线程)之间的可见性与有序性
通过下面的时序图,可以看出volatile禁止指令重排序的效果:
如图,给INSTANCE加了volatile关键字之后,在24后面加上写屏障,防止前面的21构造方法重排序到写屏障后面,保证24在21之后,实现禁止指令重排序。在这样的情况下,即使0在24前面执行,也不会发生返回不完整的实例对象的情况,最终仍然能够返回完整的实例对象。
但注意,这仍不能解决指令交错的问题。即线程t2可能会在24执行前,调用getstatic读取INSTANCE引用。
总结
- volatile可保证可见性(写屏障实现,涉及缓冲一致性协议,可见x86 LOCK 指令前缀_lock前缀-CSDN博客的“4,volatile如何保证可见性”部分),禁止了指令重排序(通过读写屏障实现),保证有序性(指通过插入内存屏障来保证指令按照顺序执行),但无法解决指令交错的问题(也就是说无法保证原子性)
- synchronized可保证原子性(同一时间只有一个线程能操作加锁的代码,不会出现指令交错的问题),可见性(通过JMM中关于lock和unlock的先行发生规则来保证:线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见)以及有序性(指持有相同锁的两个同步块只能串行的进入,即被加锁的内容要按照顺序被多个线程执行,使块与块之间有序),但不禁止指令重排序(同步块内部还是会发生重排序)。
- volatile和synchronized实现的有序性概念不同,具体可见synchronized 不是可以保证有序性的吗?volatile的有序性?synchronized 不能够保证指令重排吗?-CSDN博客