volatile关键字详解

目录

1、内存与CPU

2、缓存一致性协议

3、MESI协议

4、MESI的时间瓶颈

5、MESI解决方案的缺点

6、MESI最终解决方案:内存屏障

7、JVM内存屏障

7.1、LoadLoad屏障

7.2、StoreStore屏障

7.3、LoadStore屏障

7.4、StoreLoad屏障

8、volatile变量

9、其他

9.1、概念

9.2、缺点

9.3、修补


1、内存与CPU

现代的CPU都是多核处理器,鉴于CPU直接读取电脑内存时速度较慢,就像为了弥补CPU-磁盘之间的速度差异,而使用内存作为桥梁,同样的,为了弥补CPU-内存之间的速度差异,每个CPU内核都自带一个缓存区Cache来作为桥梁,因此,形成了如下流程:CPU-缓存区Cache-电脑内存;

一个缓存区Cache可以分为多个缓存行Cache line,缓存行Cache line是和内存进行数据交换的最小单位。每个缓存行Cache line包含3部分:

①valid用于标识该数据的有效性,如果有效位为false,CPU核心就从内存中读取,并将对应旧的缓存行数据覆盖,否则为true则继续使用旧缓存数据;

②tag用于指示数据对应的内存地址;

③block则用以存储数据,

如果涉及并发任务,

不同核CPU的缓存区Cache从电脑内存读读取同一个变量值后,在各自Thread进行写操作,进而导致不同核CPU的缓存区Cache之间、缓存区Cache与电脑CPU之间造成数据不一致,为了解决这个问题,提出了缓存一致性协议

2、缓存一致性协议

该协议的核心在于:确保电脑内存与不同CPU的缓存区Cache中的数据一致;

为了达成缓存一致性的目的,目前有2种方式:

①写失效(Write Invalidate):当一个核心修改了一份数据,其它核心如果有这份数据,就把valid标识为无效;

②写更新(Write update):当一个核心修改了一份数据,其它核心如果有这份数据,就都更新为新值,并且还是标记valid有效;

目前,实现缓存一致性的协议中,使用最多的是MESI协议,该协议根据写失效的思路来达到缓存一致性协议,该协议对传统的缓存区Cache进行了重新设计;

3、MESI协议

既然MESI协议对传统的缓存区Cache进行了重新设计,其主要是是对valid进行了修改:原先的valid从1个bit,修改为2个bit,其代表状态从原先的2种变成4种:

①M(Modified):表示核心的数据被修改了,缓存数据属于有效状态,但是数据只处于本核心对应的缓存,还没有将这个新数据写到内存中。由于此时数据在各个核心缓存区只有唯一一份,不涉及缓存一致性问题;

②E(Exclusive):表示数据只存在本核心对应的缓存中,别的核心缓存没这个数据,缓存数据属于有效状态,并且该缓存中的最新数据已经写到内存中了。同样由于此时数据在各个核心缓存区只有一份,也不涉及缓存一致性问题;

③S(Shared):表示数据存于多个核心对应的缓存中,缓存数据属于有效状态,和内存一致。这种状态的值涉及缓存一致性问题;

④I(Invalid):表示该核心对应的缓存数据无效。

因此,为了保证缓存一致性,每个核心在写操作前,需要确保其他核心已经置同一变量的缓存行valid状态位为Invalid后,再把新数据写到自己的缓存行,并之后写到内存中,为此,MESI协议包含以下几个行为:

①读(Read):当某个核心需要某个变量的值,并且该核心对应的缓存没这个变量时,就会发出读命令,希望别的核心缓存或者内存能给该核心最新的数据;

②读命令反馈(Read Response):读命令反馈是对读命令的回应,包含了之前读命令请求的数据;

③无效化(Invalidate):无效化指令是一条广播指令,它告诉其他所有核心,缓存中某个变量已经无效了。如果变量是独占的,只存在某一个核心对应的缓存区中,那就不存在缓存一致性问题了,直接在自己缓存中改了就行,也不用发送无效化指令;

④无效化确认(Invalidate Acknowledge):该指令是对无效化指令的回复,收到无效化指令的核心,需要将自己缓存区对应的变量状态改为Invalid,并回复无效化确认,以此保证发送无效化确认的缓存已经无效了;

⑤读无效(Read Invalidate):这个命令是读命令和无效化命令的综合体。它需要接受读命令反馈和无效化确认;

⑥写回(Writeback):这个命令的意思是将核心中某个缓存行对应的变量值写回到内存中去;

4、MESI的时间瓶颈

鉴于MESI协议需要确认命令来进行回复,因此影响MESI时间效率的瓶颈主要有2个

①无效化指令:某核心需要通知所有的核心,该变量对应的缓存在其他核心中是无效的。在通知完之前,该核心不能做任何关于这个变量的操作;

②确认响应:某核心需要收到其他核心的确认响应。在收到确认消息之前,该核心不能做任何关于这个变量的操作,需要持续等待其他核心的响应,直到所有核心响应完成,将其对应的缓存行标志位设为Invalid,才能继续其它操作;

针对这2个缺陷,目前针对性的有2个解决方案:

①针对无效化指令的加速:在缓存的基础上,新增Store Buffer硬件存储结构,即核心先将变量写入Store Buffer,然后再处理其他事情,如果后面的操作需要用到这个变量,就可以从Store Buffer中读取变量的值,核心读数据的順序变成Store Buffer → 缓存Cache → 内存,从而这样在任何时候核心都不用卡住,做不了关于这个变量的操作了:

②针对确认响应的加速:在缓存的基础上,新增Invalidate Queue硬件结构,其他核心收到某个核心的Invalidate的命令后,立即给该核心回Acknowledge,并把Invalidate这个操作,先记录到Invalidate Queue里,当其他操作结束时,再从Invalidate Queue中取命令,进行Invalidate操作,因此,当某个核心收到确认响应时,其他核心对应的缓存行可能还没完全置为Invalid状态;

5、MESI解决方案的缺点

①既然Store Buffer充当了CPU与缓存Cache之间的桥梁,那么,在缓存Cache收到其他核CPU的Invalidate命令确认之间,CPU还是继续使用Store Buffer里面的旧数据;

②根据Invalidate Queue定义,如果核1收到变量a=1的指令并放入Invalidate Queue中,但核1的CPU继续按照a=2的命令去执行,当核1的CPU执行完之后再去执行Invalidate Queue命令,此刻为时已晚;

既然这2个针对性解决方案还有缺点,那该怎么解决呢?

6、MESI最终解决方案:内存屏障

内存屏障,简单来讲就是一行命令,规定了某个针对缓存的操作,常用的内存屏障指令有2个:写屏障和读屏障;

①针对Store Buffer: 某核CPU在后续变量的新值写入之前,把Store Buffer的所有值刷新到缓存Cache,然后该核CPU要么就等待刷新完成后写入,要么就把后续变量的新值放到Store Buffer中,直到Store Buffer的数据按顺序刷入缓存,这种也称为内存屏障中的写屏障(Store Barrier)

举个例子:某核CPU的缓存Cache收到40个变量的最新值,此时,该CPU先把Store Buffer中40个变量全部刷新到缓存Cache中,在刷新的过程中,CPU就2种情况:

要么等Store Buffer把这40个变量刷新完之后,再从缓存Cache把最新数据写入到Store Buffer;

要么Store Buffer一边在把40个变量刷新到缓存Cache的同时,缓存Cache也一边把最新数据写入Store Buffer,直到Store Buffer把这40个变量都刷新到缓存Cache中为止;

相当于,CPU必须等待缓存Cache与Store Buffer数据完全交换完成之后(这里的交换策略有2个),CPU再从Store Buffer中获取最新数据,从而避免Store Buffer与缓存Cache数据的不一致性;

②针对Invalidate Queue:执行后需等待Invalidate Queue完全应用到缓存后,后续的读操作才能继续执行,保证执行前后的读操作对其他CPU而言是顺序执行的,这种也称为内存屏障中的读屏障(Load Barrier)

举个例子:假如Invalidate Queue中有40个变量要设置为Invalid,CPU必须等待Invalidate Queue对应的指令执行完毕,缓存Cache对应的40个变量都置为Invalid状态,此刻,CPU如果需要这40个变量时,需要从电脑内存中读取最新的数据,这样我就能保证,该核CPU必须按照Invalidate Queue中收到指令的顺序去分别执行;

这里的写读,指的是Store Buffer,其写入缓存Cache,其从缓存Cache读取;

7、JVM内存屏障

JVM也采取了内存屏障,一共有四种,也只不过是读屏障和写屏障的组合:

7.1、LoadLoad屏障

第一大段读数据指令;

LoadLoad;

第二大段读数据指令;

LoadLoad指令作用:在第二大段读数据指令被访问前,保证第一大段读数据指令执行完毕

7.2、StoreStore屏障

第一大段写数据指令;

StoreStore;

第二大段写数据指令;

StoreStore指令作用:在第二大段写数据指令被访问前,保证第一大段写数据指令执行完毕

7.3、LoadStore屏障

第一大段读数据指令;

LoadStore;

第二大段写数据指令;

LoadStore指令作用:在第二大段写数据指令被访问前,保证第一大段读数据指令执行完毕。

7.4、StoreLoad屏障

第一大段写数据指令;

StoreLoad;

第二大段读数据指令;

StoreLoad指令作用:在第二大段读数据指令被访问前,保证第一大段写数据指令执行完毕。

8、volatile变量

①针对volatile修饰变量的写操作:在写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;

②针对volatile修饰变量的读操作:在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;

通过这种方式,就可以保证被volatile修饰的变量具有线程间的可见性和禁止指令重排序的功能了。

9、其他

9.1、概念

volatile关键字可以确保,在多线程环境下,volatile变量被修改后,立马从CPU内存同步到电脑内存,进而保证某一线程修改volatile变量后,其他线程立马能看到修改后的结果;

从上面这段话中,我们可以得出3个结论:

①每次修改volatile变量后,都会从CPU内存同步到电脑主存中;

②每次读取volatile变量,都会强制从电脑主存读取最新的值;

③使用volatile会增加性能开销;

9.2、缺点

volatile解决的是多线程间共享变量的可见性问题,如果某些操作自身没法确保其原子性,那么即便使用volatile变量也无法解决该问题,典型代表:多线程环境下的i++、++i;

9.3、修补

解决i++或者++i这样的线程同步问题,可以需要使用synchronized,或者,使用Java提供的java.util.concurrent.atomic.AtomicXX提供线程安全的基本类型包装类;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值