volatile原理

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博客

参考

  1. 主要参考:b站黑马程序员JUC并发编程-满一航
  2. JVM系列之:从汇编角度分析Volatile-腾讯云开发者社区-腾讯云 (tencent.com)
  3. x86 LOCK 指令前缀_lock前缀-CSDN博客
  4. synchronized 不是可以保证有序性的吗?volatile的有序性?synchronized 不能够保证指令重排吗?-CSDN博客
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值