深入理解volatile关键字的底层工作原理

前言

在并发编程中,我们常常会用到volatile关键字修饰共享变量,来保证这个共享变量在线程之间的可见性。我们来深入了解一下volatile的底层工作原理。

volatile常见面试题:

1、volatile的三大特性?
2、为什么volatile可以保证可见性?
3、为什么volatile不可以保证原子性?

首先了解一下数据存储等级和JMM内存规范

数据存储等级

在数据存储中是分为七个等级的,第0级是寄存器,是用汇编语言操作的,第一二三级是CPU的高速缓冲区,用来缓存主内存中的数据。以上的数据存储是更小更快且储存成本更高,以下的数据存储是更大更慢且存储成本更低。第四级是主内存,就是方法区和堆内存,第五级是本地磁盘,第六级是远程的二级存储,比如web服务器或者静态资源服务器。
深入理解volatile关键字的底层工作原理

JMM内存规范

JMM八大同步规范
(1)lock(锁定):作用于 主内存的变量,把一个变量标记为一条线程独占状态
(2)unlock(解锁):作用于 主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
(3)read(读取):作用于 主内存的变量,把一个变量值从主内存传输到线程的 工作内存中,以便随后的load动作使用
(4)load(载入):作用于 工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
(5)use(使用):作用于 工作内存的变量,把工作内存中的一个变量值传递给执行引擎
(6)assign(赋值):作用于 工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
(7)store(存储):作用于 工作内存的变量,把工作内存中的一个变量的值传送到 主内存中,以便随后的write的操作
(8)write(写入):作用于 工作内存的变量,它把store操作从工作内存中的一个变量的值传送到 主内存的变量中
深入理解volatile关键字的底层工作原理

总线锁
当一个cpu(线程)访问到我们主内存中的数据时候,往总线发出一个Lock锁的信号,其他的线程不能够对该主内存做任何操作,变为阻塞状态。该模式,存在非常大的缺陷,就是将并行的程序,变为串行,没有真正发挥出cpu多核的好处。

MESI协议
E:独享:当只有一个cpu线程的情况下,cpu副本数据与主内存数据如果
保持一致的情况下,则该cpu状态为E状态 独享。
S:共享:在多个cpu线程的情况了下,每个cpu副本之间数据如果保持一致
的情况下,则当前cpu状态为S
M:如果当前cpu副本数据如果与主内存中的数据不一致的情况下,则当前cpu状态
为M
I: 总线嗅探机制发现 状态为m的情况下,则会将该cpu改为i状态 无效
该cpu缓存主动获取主内存的数据同步更新。如果状态是M的情况下,则使用嗅探机制通知其他的CPU工作内存副本状态为I无效状态,则 刷新主内存数据到本地中,从而多核cpu数据的一致性。

Volatile的底层实现原理
通过汇编lock前缀指令触发底层锁的机制
锁的机制两种:总线锁/MESI缓存一致性协议

主要帮助我们解决多个不同cpu之间三级缓存之间数据同步

第一步:举例说明,为什么会出现这种情况?例子:共享变量控制run方法的
第二步:我先讲一下数据的存储等级和JMM内存规范
其实cpu在执行子线程的时候,共享变量是从cpu的副本内存中读取的,并不是从主内存中读取的。cpu直接从主内存中读取数据是效率比较低的,所以cpu中就存在了一个高速缓冲区,用来缓存主内存中的数据。

问到volatile关键字:
第一步:举例说明,为什么会出现这种情况?例子:共享变量控制run方法的
第二步:讲数据的存储等级JMM内存规范,直接从主内存中读取数据慢,所以cpu中设定了一个高速缓冲区,可以通过Volatile解决问题
第三步:Volatile的底层实现原理,通过汇编lock前缀指令触发底层锁的机制,锁的机制两种:总线锁(老版本cpu)/MESI缓存一致性协议(新版本cpu)

volatile重排序

volatile伪共享问题

单例中为什么需要加上volatile关键字

存到主内存之后才会刷新状态

不保证原子性问题:
一个线程刷新了主内存,总线嗅探机制导致另一个线程的副本数据无效。
Volatile为了能够保证数据的可见性,但是不能够保证原子性,及时的将工作内存的数据刷新主内存中,导致其他的工作内存的数据变为无效状态,其他工作内存做的count++操作等于就是无效丢失了,这是为什么我们加上Volatile count结果在小于10000以内。

Cpu会以缓存行的形式读取主内存中数据,缓存行的大小为2的幂次数字节,
一般的情况下是为64个字节。
如果该变量共享到同一个缓存行,就会影响到整理性能。
例如:线程1修改了long类型变量A,long类型定义变量占用8个字节,在由于
缓存一致性协议,线程2的变量A副本会失效,线程2在读取主内存中的数据的时候,
以缓存行的形式读取,无意间将主内存中的共享变量B也读取到内存中,而化主内存
中的变量B没有发生变化。

在1.8之后因为对jvm进行了优化,所以填充字节的需要需要继承的方式填充,或者使用类注解@sun.misc.Contended ,启动的时候需要加上该参数-XX:-RestrictContended

为什么都是64字节传输效率差那么多

重排序在单线程中效率是没有改变的,在多线程中能提高效率
x.join代表等x线程执行完再执行当前线程

new过程
1、分配对象内存空间
2、构造函数初始化
3、将对象赋值给变量

重排序
1、分配对象内存空间
2、将对象赋值给变量
3、构造函数初始化
执行第二步之后对象已经被赋值,if(对象==null)判断对象已经存在,在还没初始化之前就去使用导致错误,

底层使用内存屏障防止重排序