死磕Java多线程(六)---Volatile原理

引言:

上节说到计算机的硬件组成可以抽象为由总线、IO设备、主存、处理器(CPU)等组成。其中数据存放在主存中,CPU负责指令的执行,CPU的指令执行非常快,大部分简单指令的执行只需要一个时钟周期,而一次主内存数据的读取则需要几十到几百个时钟周期,那么CPU从主存中读写数据就会有很大的延迟。这个时候就产生了高速缓存的概念。

也就是说,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据回写到主存当中,通过这种方式来降低CPU从主存中获取数据的延迟。大致的示意图如下:
在这里插入图片描述
图一这个模型,可以简单的认为是单核模型,在这个模型里面,以i++这个操作为例,程序执行时,会先从主内存中获取i的值,复制到高速缓存,然后CPU从高速缓存中加载并执行+1操作,操作完成后回写到高速缓存,最后再从高速缓存回写到主内存。单核模型这样操作没有任何问题,但是计算机自产生以来,一直追求的两个目标,一个是如何做的更多,另一个就是如何计算得更快,这样带来的变化就是单核变成多核,高速缓存分级存储。大致的示意图如下:
在这里插入图片描述
在图二示意图里面,i++这个操作就有问题了,因为多核CPU可以线程并行计算,在Core 0和Core 1中可以同时将i复制到各自缓存中,然后CPU各自进行计算,假设初始i为1,那么预期我们希望是2,但是实际由于两个CPU各自先后计算后最终主内存中的i可能是2,也可能是其他值。

这个就是硬件内存架构中存在的一个问题,缓存一致性问题,就是说核1改变了变量i的值之后,核0是不知道的,存放的还是旧值,最终对这样的一个脏数据进行操作。

为此,CPU的厂商定制了相关的规则来解决这样一个硬件问题,

在以前的解决方案

总线加锁:其实很好理解总线锁,咱们来看图二,前面提到了变量会从主内存复制到高速缓存,计算完成后,会再回写到主内存,而高速缓存和主内存的交互是会经过总线的。既然变量在同一时刻不能被多个CPU同时操作,会带来脏数据,那么只要在总线上阻塞其他CPU,确保同一时刻只能有一个CPU对变量进行操作,后续的CPU读写操作就不会有脏数据。总线锁的缺点也很明显,有点类似将多核操作变成单核操作,所以效率低。

这样的解决方法,在多核操作的今天变得越来越不适合,效率太低!!!!,为了优化效率,CPU的厂商定又制了如下规则来解决这个问题

缓存锁: 即缓存一致性协议,主要有MSI、MESI、MOSI等,这些协议的主要核心思想:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

好了,理论补充完了,开始瞎掰。。。。。。。

在JAVA开发中,当某个变量需要对所有线程可见的时候,我们可以使用关键词volatile来实现我们的需求。

我们知道Volatile保证可见性与有序性,但是不保证原子性,保证原子性需要借助synchronized这样的锁机制。

那么Volatile是如何保证可见性与有序性的呢?

假设有一个有一个Boolean类型的initFlag变量,默认为false,public boolean initFlag = false;
当我们在一个线程将其修改为true时,如果不使用Volatile关键词修饰,其它线程是不知道它已经修改成了true的。

注:请参照Java内存模型

在这里插入图片描述
使用Volatile关键词修饰后:public volatile boolean initFlag = false;
在这里插入图片描述
在这里插入图片描述

通过引言,和上面这两幅图,大家应该也知道Volatile什么怎么实现可见性和有序性了吧(重点)。

那么我们现在来总结一下

Volatile底层实现主要是通过汇编指令,他会锁定这块内存区域的缓存并回写到主内存,此操作被称为“缓存锁定”,MESI缓存一致性协议(由各大CPU厂商实现的硬件级别协议),会阻止同时修改被两个以上处理器缓存的内存区域数据(锁定store阶段到write阶段【这样就保证了有序性】《------》内存屏障,在以前的总线加锁是锁住所有阶段,锁的粒度被大大减小,所以也被称为轻量级锁,),当一个处理器的缓存值通过总线回写到内存时,其他CPU会通过CPU嗅探机制,将相应的缓存内的这个数据强制失效,只能从主缓存再次读取。这样就保证了可见性。

补充:内存屏障:
内存屏障提供了3个功能:

  1. 确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;
  2. 强制将对缓存的修改操作立即写入主存;
  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

这3个功能又是怎么做到的呢?来看下内存屏障的策略:

  1. 在每个volatile写操作前面插入storestore屏障;
  2. 在每个volatile写操作后面插入storeload屏障;
  3. 在每个volatile读操作后面插入loadload屏障;
  4. 在每个volatile读操作后面插入loadstore屏障;

最后,我们将代码转换为汇编指令来看看Java虚拟机到底是怎么实现的。
注:转换为汇编指令,可以通过-XX:+PrintAssembly来实现,window环境具体如何操作请参考此处(https://dropzone.nfshost.com/hsdis.xht)。

为什么Volatile不能保证原子性呢?

我们通过上文知道了volatile的底层只是当一个将被volatile修饰变量改变的时候将其它线程缓存中该数据失效。所以当你进行i++的时候,多个线程还是可能会当时拿到一样的值进行+1操作,但是有个线程率先执行完了,并将变量推送到主内存,所以其它线程的这个缓存这时候就都失效了,但是确实是执行了+1操作。
(白话一点就是,我做了,但是失效了)
(官方一点就是,从Load到store到内存屏障,一共4步,其中最后一步jvm让这个最新的变量的值在所有线程可见,也就是最后一步让所有的CPU内核都获得了最新的值,但中间的几步(从Load到Store)是不安全的,中间如果其他的CPU修改了值将会丢失。)

发布了45 篇原创文章 · 获赞 3 · 访问量 2325

猜你喜欢

转载自blog.csdn.net/weixin_44046437/article/details/99243420