volatile关键字(数据可见性、重排序、伪共享等)

volatile的特性

可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最 后的写入。

原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复 合操作不具有原子性(基于这点,我们通过会认为volatile不具备原子性)。volatile仅 仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个 临界区代码的执行具有原子性。 64位的long型和double型变量,只要它是volatile变量,对该变量的读/写就具有原子性。

有序性:对volatile修饰的变量的读写操作前后加上各种特定的内存屏障来禁止指令重排序来保障有序性。

volatile写-读的内存语义

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来 将从主内存中读取共享变量。

volatile可见性

volatile可见性实现原理

JMM内存交互层面实现

volatile修饰的变量的read、load、use操作和assign、store、write必须是连续的,即修 改后必须立即同步回主内存,使用时必须从主内存刷新,由此保证volatile变量操作对多线程 的可见性。

硬件层面实现

通过lock前缀指令,会锁定变量缓存行区域并写回主内存,这个操作称为“缓存锁定”, 缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回 写到内存会导致其他处理器的缓存无效。

lock前缀指令的作用

1. 确保后续指令执行的原子性。在Pentium及之前的处理器中,带有lock前缀的指令在执 行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很 大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低 lock前缀指令的执行开销。

2. LOCK前缀指令具有类似于内存屏障的功能,禁止该指令与前面和后面的读写指令重排序。

3. LOCK前缀指令会等待它之前所有的指令完成、并且所有缓冲的写操作写回内存(也就是 将store buffer中的内容写入内存)之后才开始执行,并且根据缓存一致性协议,刷新 store buffer的操作会导致其他cache中的副本失效。

这里lock前缀指令涉及到了cpu缓存架构,以及缓存一致性协议

cpu缓存架构

CPU高速缓存

CPU缓存即高速缓冲存储器,是位于CPU与主内存间的一种容量较小但速度很高的存储器。由于CPU的速度远高于主内存,CPU直接从内存中存取数据要等待一定时间周期,Cache中保存着CPU刚用过或循环使用的一部分数据,当CPU再次使用该部分数据时可从Cache中直接调用,减少CPU的等待时间,提高了系统的效率。

多CPU多核缓存架构

物理CPU:物理CPU就是插在主机上的真实的CPU硬件,在Linux下可以数不同的physical id 来确认主机的物理CPU个数。

核心数:我们常常会听说多核处理器,其中的核指的就是核心数。在Linux下可以通过cores来确认主机的物理CPU的核心数。

逻辑CPU:逻辑CPU跟超线程技术有联系,假如物理CPU不支持超线程的,那么逻辑CPU的数量等于核心数的数量;如果物理CPU支持超线程,那么逻辑CPU的数目是核心数数目的两倍。在Linux下可以通过 processors 的数目来确认逻辑CPU的数量。

现代CPU为了提升执行效率,减少CPU与内存的交互,一般在CPU上集成了多级缓存架构,常见的为三级缓存结构。

缓存一致性(Cache coherence)

计算机体系结构中,缓存一致性是共享资源数据的一致性,这些数据最终存储在多个本地缓存中。当系统中的客户机维护公共内存资源的缓存时,可能会出现数据不一致的问题,这在多处理系统中的cpu中尤其如此。

在共享内存多处理器系统中,每个处理器都有一个单独的缓存内存,共享数据可能有多个副本:一个副本在主内存中,一个副本在请求它的每个处理器的本地缓存中。当数据的一个副本发生更改时,其他副本必须反映该更改。缓存一致性是确保共享操作数(数据)值的变化能够及时地在整个系统中传播的规程。

缓存一致性的要求

写传播(Write Propagation)

对任何缓存中的数据的更改都必须传播到对等缓存中的其他副本(该缓存行的副本)。

事务串行化(Transaction Serialization)

对单个内存位置的读/写必须被所有处理器以相同的顺序看到。理论上,一致性可以在加载/存储粒度上执行。然而,在实践中,它通常在缓存块的粒度上执行。

一致性机制(Coherence mechanisms)

确保一致性的两种最常见的机制是窥探机制(snooping )和基于目录的机制(directory-based),这两种机制各有优缺点。如果有足够的带宽可用,基于协议的窥探往往会更快,因为所有事务都是所有处理器看到的请求/响应。其缺点是窥探是不可扩展的。每个请求都必须广播到系统中的所有节点,这意味着随着系统变大,(逻辑或物理)总线的大小及其提供的带宽也必须增加。另一方面,目录往往有更长的延迟(3跳 请求/转发/响应),但使用更少的带宽,因为消息是点对点的,而不是广播的。由于这个原因,许多较大的系统(>64处理器)使用这种类型的缓存一致性。

窥探协议类型

根据管理写操作的本地副本的方式,有两种窥探协议:

Write-invalidate(写过期)

当处理器写入一个共享缓存块时,其他缓存中的所有共享副本都会通过总线窥探失效。这种方法确保处理器只能读写一个数据的一个副本。其他缓存中的所有其他副本都无效。这是最常用的窥探协议。MSI、MESI、MOSI、MOESI和MESIF协议属于该类型。

Write-update (写更新)

当处理器写入一个共享缓存块时,其他缓存的所有共享副本都会通过总线窥探更新。这个方法将写数据广播到总线上的所有缓存中。它比write-invalidate协议引起更大的总线流量。这就是为什么这种方法不常见。Dragon和firefly协议属于此类别。

一致性协议(Coherence protocol)

一致性协议在多处理器系统中应用于高速缓存一致性。为了保持一致性,人们设计了各种模型和协议,如MSI、MESI(又名Illinois)、MOSI、MOESI、MERSI、MESIF、write-once、Synapse、Berkeley、Firefly和Dragon协议。

MESI协议

MESI协议是一个基于写失效的缓存一致性协议,是支持回写(write-back)缓存的最常用协议。也称作伊利诺伊协议 (Illinois protocol,因为是在伊利诺伊大学厄巴纳-香槟分校被发明的)。与写通过(write through)缓存相比,回写缓冲能节约大量带宽。总是有“脏”(dirty)状态表示缓存中的数据与主存中不同。MESI协议要求在缓存不命中(miss)且数据块在另一个缓存时,允许缓存到缓存的数据复制。与MSI协议相比,MESI协议减少了主存的事务数量。这极大改善了性能。

状态

缓存行有4种不同的状态:

已修改Modified (M)

缓存行是脏的(dirty),与主存的值不同。如果别的CPU内核要读主存这块数据,该缓存行必须回写到主存,状态变为共享(S).

独占Exclusive (E)

缓存行只在当前缓存中,但是干净的--缓存数据同于主存数据。当别的缓存读取它时,状态变为共享;当前写数据时,变为已修改状态。

共享Shared (S)

缓存行也存在于其它缓存中且是未修改的。缓存行可以在任意时刻抛弃。

无效Invalid (I)

缓存行是无效的

伪共享的问题

如果多个核的线程在操作同一个缓存行中的不同变量数据,那么就会出现频繁的缓存失效,即使在代码层面看这两个线程操作的数据之间完全没有关系。这种不合理的资源竞争情况就是伪共享(False Sharing)。

数据X、Y、Z被加载到同一Cache Line中,线程A在Core1上修改X,而修改X会导致其所在的所有核上的缓存行均失效;假设此时线程B在Core2上读取Y,由于X所在的缓存行已经失效,所有Core2必须从内存中重新读取。线程A的操作不会修改Y,但是由于X和Y共享的是一个缓存行,就导致线程B不能很好地利用Cache,这其实就是伪共享。简单来说,伪共享指的是由于共享缓存行导致缓存无效的场景。
 

避免伪共享方案

1.缓存行填充

通过对齐填充解决伪共享的问题;

使对象是64个字节或者是64的倍数;

空间换时间,让同一个地址块不会被多个线程竞争。

2.使用 @sun.misc.Contended 注解(java8)

在JDK1.8中,新增了一种注解@sun.misc.Contended来使各个变量在Cache Line中分隔开。其原理是在使用此注解的对象或字段的前后各增加128字节大小的padding,使用2倍于大多数硬件缓存行的大小来避免相邻扇区预取导致的伪共享冲突。可以在类前或属性前加上此

volatile有序性

指令重排序

Java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况 的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。

指令重排序的意义:JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机 器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。 在编译器与CPU处理器中都能执行指令重排优化操作

volatile禁止重排序场景:

1. 第二个操作是volatile写,不管第一个操作是什么都不会重排序

2. 第一个操作是volatile读,不管第二个操作是什么都不会重排序

3. 第一个操作是volatile写,第二个操作是volatile读,也不会发生重排序

JMM内存屏障插入策略

1. 在每个volatile写操作的前面插入一个StoreStore屏障

2. 在每个volatile写操作的后面插入一个StoreLoad屏障

3. 在每个volatile读操作的后面插入一个LoadLoad屏障

4. 在每个volatile读操作的后面插入一个LoadStore屏障

x86处理器不会对读-读、读-写和写-写操作做重排序, 会省略掉这3种操作类型对应的内存 屏障。仅会对写-读操作做重排序,所以volatile写-读操作只需要在volatile写后插入 StoreLoad屏障

JVM层面的内存屏障

在JSR规范中定义了4种内存屏障:

LoadLoad屏障:(指令Load1; LoadLoad; Load2),在Load2及后续读取操作要读取的数 据被访问前,保证Load1要读取的数据被读取完毕。

LoadStore屏障:(指令Load1; LoadStore; Store2),在Store2及后续写入操作被刷出前, 保证Load1要读取的数据被读取完毕。

StoreStore屏障:(指令Store1; StoreStore; Store2),在Store2及后续写入操作执行前, 保证Store1的写入操作对其它处理器可见。

StoreLoad屏障:(指令Store1; StoreLoad; Load2),在Load2及后续所有读取操作执行 前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的 实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

由于x86只有store load可能会重排序,所以只有JSR的StoreLoad屏障对应它的mfence或 lock前缀指令,其他屏障对应空操作

硬件层内存屏障 硬件层提供了一系列的内存屏障 memory barrier / memory fence(Intel的提法)来提供 一致性的能力。拿X86平台来说,有几种主要的内存屏障:

1. lfence,是一种Load Barrier 读屏障

2. sfence, 是一种Store Barrier 写屏障

3. mfence, 是一种全能型的屏障,具备lfence和sfence的能力

4. Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对 CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。

内存屏障有两个能力:

1. 阻止屏障两边的指令重排序

2. 刷新处理器缓存/冲刷处理器缓存

对Load Barrier来说,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主 内存加载数据;对Store Barrier来说,在写指令之后插入写屏障,能让写入缓存的最新数据写 回到主内存。

Lock前缀实现了类似的能力,它先对总线和缓存加锁,然后执行后面的指令,最后释放锁 后会把高速缓存中的数据刷新回主内存。在Lock锁住总线的时候,其他CPU的读写请求都会被 阻塞,直到锁释放。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值