高并发学习笔记4-内存屏障

说明:本博客仅为个人学习笔记,无任何商业用途。

1、前言

上文我们讲到引入写缓存器和无效化队列的优缺点,本文我们讨论可见性和重排序相关的问题。

2、产生背景

写缓冲器

对于变量的更新操作如果没有写到相应的处理器的高速缓存中,对其他处理器来说仍然不可见的,因为写缓冲器是处理器私有的东西。写缓冲器写入高速缓存的条件是,接收到了其他处理器的Invalidate Acknowledge的消息。若在此期间出现的读操作就会出现可见性问题

无效化队列

读取数据时候,如果没有将无效化队列中相应的缓存条目置为 I,则可能导致读取的数据任然是旧值

3、屏障分类

在这里插入图片描述
StoreStore

StoreStore屏障可以通过对写缓冲器中的条目进行标记来实现禁止StoreStore重排序。StoreStore屏障会将写缓冲器中的现有条目进行标记,代表这些条目的写操作要先于屏障之后的写操作被提交。处理器执行写操作时发现写缓冲器存在被标记的缓存条目(当前写对应条目被标记),即使当前写操作对应缓存条目的状态为E或M,此时处理器也不直接将写操作的数据写入高速缓存,而是将其写入写冲器,从而使得StoreStore屏障之前的任何写操作优先于该屏障之后的写操作被提交

LoadLoad

LoadLoad屏障是通过清空无效化队列来实现禁止LoadLoad重排序的。LoadLoad屏障会使其执行处理器根据无效化队列中的Invalidate消息删除其高速缓存中相应的副本。这个过程也成为将无效化队列应用到高速缓存,也被称为清空无效化队列,它使处理器有机会将其他处理器对共享变量所做的更新同步到该处理器的高速缓存中,从而消除LoadLoad重排序根源。根源就是写缓存器,未及时将其中的内容写入到高速缓存中

StoreLoad

处理器往往将StoreLoad屏障实现为一个通用基本内存屏障,即StoreLoad能够实现其他三种内存屏障的效果。虽然能够代替其他内存屏障,但是其开销也是最大的,StoreLoad屏障会清空无效化队列,并将写缓冲器中的条目冲刷(写入)高速缓存。因此,StoreLoad屏障既可以将其他处理器对共享变量所作的更新同步到该处理器的高速缓存中,又可以使其执行处理器对共享变量所做的改变对其他处理器来说是可同步的。

写线程的执行处理器所执行的存储屏障保障了该线程对共享变量所做的更新对读线程来说是可同步的(冲刷写缓存器) 读线程的执行处理器所执行的加载屏障将写线程对共享变量所做的更新同步到该处理器的高速缓存中(清空无效化队列)

思考
使用基本屏障后就可以保证不重排序了吗?答案是不一定!

基本内存屏障并不禁止重排序,XY屏障两侧的内存操作仍然可以在不越过内存屏障本身的情况下在各自的范围里进行重排序,并且XY屏障左侧的非X操作与屏障右侧的非Y操作之间仍然可以进行重排序(即越过屏障本身)

在这里插入图片描述
指令序列中Load2、Load3和Store1、Store2之间无法进行从排序,而Store1、Load1和Store2之间可以重排序,Store3、Load2和Load3之间可以重排序,Load1和Store3之间也可以进行重排序

拓展:

按照可见性保障来划分,可以分为加载屏障和存储屏障:
1、加载屏障:刷新处理器缓存
2、存储屏障:冲刷处理器缓存
按照有序性保障来划分,可以分为获取屏障和释放屏障:
1、获取屏障:读操作包括Read-modify-Writer以及普通的读操作)之间插入该屏障,其作用禁止该读操作与其后面的任何读写操作之间进行重排序。
2、释放屏障:写操作之前插入该内存屏障,其作用是禁止该写操作与前面的任何读写操作之间进行重排序。

4、JVM对屏障的优化

java虚拟机对内存屏障优化常常包括省略、合并等。例如对两个连续的volatile写操作,Java虚拟机可能只在最后一个volatile写操作之后插入StoreLoad屏障,而不是每个volatile写操作插入一个StoreLoad屏障。在x86处理器下,Java虚拟机对monitorexit的实现本身就带有StoreLoad屏障效果,因此java虚拟机不会再monitorexit对应的机器码指令之后插入StoreLoad屏障

5、java同步机制

Java虚拟机对synchronized、volatile和final关键字的语义的实现就是借助内存屏障的。获取屏障和释放屏障相当于由基本内存屏障组合而成的复合屏障。获取屏障相当于LoadLoad屏障和LoadStore屏障的组合,它能够禁止该屏障之前的任何读操作与该屏障之后的任何读、写操作之间进行重排序释放屏障相当于LoadStore屏障和StoreStore屏障的组合,它能够禁止该屏障之前任何读、写操作与该屏障之后的任何写操作之间进行重排序

synchronized

synchronized关键字包括monitor enter 和monitor exit两个jvm指令,它能够保证在任何时候任何线程执行到monitor enter之前都必须从主内存中获取数据,而不是从缓存中,在monitor exit成功运行之后,共享变量被更新后的值必须刷入主内存。

Java虚拟机(JIT编译器)会在monitorenter(用于申请锁的字节码指令)对应的指令后临界区开始前的地方插入一个获取屏障。java虚拟机会在临界区结束后monitorexit(用于释放锁的字节码指令)对应的指令前的地方插入一个释放屏障。这里,获取屏障和释放屏障一起保障了临界区内的任何读、写操作都无法被重排序到临界区之外(临界区内仍然可以重排序,只是无法重排序到外面),再加上锁的排他性,这使得临界区内的操作具有原子性

synchronized关键字对有序性的保障与volatile关键字对有序性的保障实现原理是一样的,也是通过释放屏障和获取屏障的配对使用实现。释放屏障使得写线程在临界区中执行的读、写操作先于monitorexit对应的指令(相当于写操作)被提交,而获取屏障使读线程必须在获得锁(相当于read-modify-write操作)之后才能够执行临界区中的操作。写线程以及读线程通过这种释放屏障和获取屏障的配对使用实现了有序性。

Java虚拟机也会在monitorexit对应的指令(相当于写操作)之后插入一个StoreLoad屏障。这个处理的目的与在volatile写操作之后插入一个StoreLoad屏障类似。该屏障充当了存储屏障,从而确保锁的持有线程在释放锁之前所执行的所有操作的结果能够到达高速缓存,并消除存储转发的副作用。另外该屏障禁止了monitorexit对应的指令与其他同步块的monitorenter对应指令进行重排序,这保障了monitorenter与monitorexit总是成对的,从而使synchronized块的并列(一个块之后又有一个synchronized块)以及synchronized块的嵌套(synchronized块内包含了其他synchronized块—>synchronized为可重入锁)成为可能

在这里插入图片描述

volatile

Java虚拟机(JIT编译器)在volatile变量写操作之前插入的释放屏障使得该屏障之间的任何读、写操作都先于这个volatile变量写操作被提交,而java虚拟机(JIT编译器)在volatile变量读取操作之后插入的获取屏障使得这个volatile变量读操作先于该屏障之后的任何读、写操作被提交。写线程和读线程通过各自执行的释放屏障和获取屏障保障了有序性。

需要注意的是,释放屏障只是确保了该屏障之间的读、写操作先于该屏障之后的任何写操作被提交,因此释放屏障之前的操作之间,其提交顺序可以与程序不一致。针对写线程对A,B的更新,处理器无须保证对A的更新先于对B的更新被提交。而只需要保证对A以及B的更新先于对V的更新被提交即可。类似的,获取屏障只是确保了该屏障之前的任何读操作先于该屏障之后的任何读、写操作被提交。因此获取屏障之后的操作之间,其提交顺序可以与程序顺序不一致。写线程和读线程通过配对使用释放屏障和获取屏障,使得上述内存操作提交顺序与程序顺序不一致并不会对有序性产生影响。

java虚拟机(JIT编译器)会在volatile变量写操作之后插入一个StoreLoad屏障。该屏障不仅禁止该屏障之后的任何读操作与该屏障之前的任何写操作(包括volatile写操作)之间进行重排序,除此之外还起以下两个作用:
充当存储屏障。 StoreLoad屏障是一个通用存储屏障,其功能涵盖了其他3个基本内存屏障。StoreLoad屏障通过清空其执行处理器的写缓冲器使得该屏障前的所有写操作(包括volatile写操作以及其他任何写操作)的结果得以到达高速缓存,从而使这些更新对其他处理器而言使可同步的。
·充当加载屏障,以消除存储转发的副作用。假设处理器processor0在t1时刻更新了某个volatile变量,在随后的t2时刻又读取了该变量。由于存储转发技术可能使得一个处理器无法将其他处理器对共享变量所做的更新同步到该处理器的高速缓存上,而Java语言规范有要求volatile读操作总是可以读取到其他处理器相对应变量所做的更新,因此java虚拟机需要在volatile变量写操作和随后的volatile变量读操作之间插入一个StoreLoad屏障。这是利用了StoreLoad屏障既能够清空写缓冲器还能清空无效化队列的功能,从而使处理器对volatile变量所做的更新能够被同步到volatile变量读线程的执行处理器上(后续同步就是MESI实现) java虚拟机(JIT编译器)在volatile变量读操作之前插入的一个加载屏障相当于LoadLoad屏障,它通过清空无效化队列来使得其后的读操作(包括volatile读操作)有机会读取到其他处理器对共享变量所做的更新。读线程能够读取到写线程对volatile变量所做的更新,有赖于写线程在volatile写操作后所执行的存储屏障。可见volatile对可见性的保障使通过写线程、读线程配对使用存储屏障和加载屏障实现的。 由于x86处理器仅支持StoreLoad重排序,因此在x86处理器下Java虚拟机会将其他屏障映射为空指令。也就是说x86处理器下的 Java虚拟机无序在volatile读操作前、volatile读操作后以及volatile写操作前插入任何指令,而只需要在volatile写操作后插入一个StoreLoad屏障,这个屏障在Hotspot虚拟机中是由一个Lock前缀的空操作指令充当(具体指令:“lock addl $0x0,(%rsp)”,lock前缀指令能够清空写缓冲器,而x86处理器并没有使用无效化队列,因此该指令就起到了StoreLoad屏障的作用)。

在这里插入图片描述

6、小结

从操作系统的角度来看,通过屏障的混合使用消除了重排序和可见性的问题,那从JMM角度来思考呢?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值