多线程基础——内存屏障

内存屏障用于防止指令重排序,确保多线程环境中的数据正确性。硬件层面,Intel CPU提供了如lfence、sfence和mfence等指令实现内存屏障。在Java中,内存屏障体现在volatile和final的语义中,以及锁和CAS操作中,确保多线程间的内存可见性和有序性。Java内存模型屏蔽了底层硬件平台差异,由JVM生成相应机器码实现屏障效果。

内存屏障

内存屏障(memory barrier)是一种概念。编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。

例如:一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。

内存屏障正是通过阻止屏障两边的指令重排序来避免编译器和硬件的不正确优化而提出的一种解决办法

内存屏障有2个作用:

  • 阻止屏障两侧的指令重排序;
  • 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

内存屏障由来

当前CPU大多数采用的是write back策略。因为大多数情况下,CPU异步完成写内存产生的部分延迟是可以接受的,而且这个延迟极短。只有在多线程环境下需要严格保证内存可见等极少数特殊情况下才需要保证CPU的写在外界看来是同步完成的,需要借助CPU提供的内存屏障实现。如果直接采用write through,那每次写内存都需要等待数据刷入内存,极大影响了CPU的执行效率。

硬件层的内存屏障

Intel硬件提供了一系列的汇编指令串行化运行读写指令达到内存屏障(保证读写有序性)的目的: 

  • lfence:  是一种Load Barrier 读屏障 
  • sfence: 是一种Store Barrier 写屏障 
  • mfence:是一种全能型的屏障,具备ifence和sfence的能力 
  • Lock前缀:Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令

Load Barrier:在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制重新从主内存加载数据

Store Barrier:在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。

Java内存屏障

Java的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。

  • LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  • LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  • StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码

volatile语义中的内存屏障

final语义中的内存屏障

对于final域,编译器和CPU会遵循两个排序规则:

  1. 新建对象过程中,构造体中对final域的初始化写入和这个对象赋值给其他引用变量,这两个操作不能重排序;
  2. 初次读包含final域的对象引用和读取这个final域,这两个操作不能重排序;(意思就是先赋值引用,再调用final值)
  • 总必需保证一个对象的所有final域被写入完毕后才能引用和读取。这也是内存屏障的起的作用。
  • 写final域:在编译器写final域完毕,构造体结束之前,会插入一个StoreStore屏障,保证前面的对final写入对其他线程/CPU可见,并阻止重排序。
  • 读final域:在上述规则2中,两步操作不能重排序的机理就是在读final域前插入了LoadLoad屏障。
  • X86处理器中,由于CPU不会对写-写操作进行重排序,所以StoreStore屏障会被省略;而X86也不会对逻辑上有先后依赖关系的操作进行重排序,所以LoadLoad也会变省略。

CAS

在CPU架构中依靠lock信号保证可见性并禁止重排序。
lock前缀是一个特殊的信号,执行过程如下:

  • 对总线和缓存上锁。
  • 强制所有lock信号之前的指令,都在此之前被执行,并同步相关缓存。
  • 执行lock后的指令(如cmpxchg)。
  • 释放对总线和缓存上的锁。
  • 强制所有lock信号之后的指令,都在此之后被执行,并同步相关缓存。

因此,lock信号虽然不是内存屏障,但具有mfence的语义(当然,还有排他性的语义)。
与内存屏障相比,lock信号要额外对总线和缓存上锁,成本更高。

JVM的内置锁通过操作系统的管程实现。由于管程是一种互斥资源,修改互斥资源至少需要一个CAS操作。因此,锁必然也使用了lock信号,具有mfence的语义。

 

java内存屏障使用

常见的有以下几种:

  • 通过 Synchronized关键字包住的代码区域,当线程进入到该区域读取变量信息时,保证读到的是最新的值.这是因为在同步区内对变量的写入操作,在离开同步区时就将当前线程内的数据刷新到内存中,而对数据的读取也不能从缓存读取,只能从内存中读取,保证了数据的读有效性.这就是插入了StoreStore屏障
  • 使用了volatile修饰变量,则对变量的写操作,会插入StoreLoad屏障.
  • 其余的操作,则需要通过Unsafe这个类来执行;UNSAFE.putOrderedObject类似这样的方法,会插入StoreStore内存屏障;Unsafe.putVolatiObject 则是插入了StoreLoad屏障

文献

java内存屏障的原理与应用_breakout_alex的博客-优快云博客_java 内存屏障

【JMM】内存模型之内存屏障 - Java 技术驿站

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值