内存屏障及其在-JVM 内的应用(下)

做者:LeanCloud 后端高级工程师 郭瑞html

内容分享视频版本: 内存屏障及其在-JVM-内的应用java

Java Memory Model (JMM)

Java 为了能在不一样架构的 CPU 上运行,提炼出一套本身的内存模型,定义出来 Java 程序该怎么样和这个抽象的内存模型进行交互,定义出来程序的运行过程,什么样的指令能够重排,什么样的不行,指令之间可见性如何等。至关因而规范出来了 Java 程序运行的基本规范。这个模型定义会很不容易,它要有足够弹性,以适应各类不一样的硬件架构,让这些硬件在支持 JVM 时候都能知足运行规范;它又要足够严谨,让应用层代码编写者能依靠这套规范,知道程序怎么写才能在各类系统上运行都不会有歧义,不会有并发问题。linux

在著名的 《深刻理解 Java 虚拟机》一书的图 12-1 指出了在 JMM 内,线程、主内存、工做内存的关系。图片来自该书的 Kindle 版:git

从内存模型一词就能看出来,这是对真实世界的模拟。图中 Java 线程对应的就是 CPU,工做内存对应的就是 CPU Cache,Java 提炼出来的一套 Save、Load 指令对应的就是缓存一致性协议,就是 MESI 等协议,最后主内存对应的就是 Memory。真实世界的硬件须要根据自身状况去向这套模型里套。github

JMM 完善于 JSR-133,如今通常会把详细说明放在 Java Language 的 Spec 上,好比 Java11 的话在:Chapter 17. Threads and Locks。在这些说明以外,还有个特别出名的 Cookbook,叫 The JSR-133 Cookbook for Compiler Writers后端

JVM 上的 Memory Barrier

JVM 按先后分别有读、写两种操做以全排列方式一共提供了四种 Barrier,名称就是左右两边操做的名字拼接。好比 LoadLoad Barrier 就是放在两次 Load 操做中间的 Barrier,LoadStore 就是放在 Load 和 Store 中间的 Barrier。Barrier 类型及其含义以下:缓存

  • LoadLoad,操做序列 Load1, LoadLoad, Load2,用于保证访问 Load2 的读取操做必定不能重排到 Load1 以前。相似于前面说的 Read Barrier,须要先处理 Invalidate Queue 后再读 Load2;
  • StoreStore,操做序列 Store1, StoreStore, Store2,用于保证 Store1 及其以后写出的数据必定先于 Store2 写出,即别的 CPU 必定先看到 Store1 的数据,再看到 Store2 的数据。可能会有一次 Store Buffer 的刷写,也可能经过全部写操做都放入 Store Buffer 排序来保证;
  • LoadStore,操做序列 Load1, LoadStore, Store2,用于保证 Store2 及其以后写出的数据被其它 CPU 看到以前,Load1 读取的数据必定先读入缓存。甚至可能 Store2 的操做依赖于 Load1 的当前值。这个 Barrier 的使用场景可能和上一节讲的 Cache 架构模型很难对应,毕竟那是一个极简结构,而且只是一种具体的 Cache 架构,而 JVM 的 Barrier 要足够抽象去应付各类不一样的 Cache 架构。若是跳出上一节的 Cache 架构来讲,我理解用到这个 Barrier 的场景多是说某种 CPU 在写 Store2 的时候,认为刷写 Store2 到内存,将其它 CPU 上 Store2 所在 Cache Line 设置为无效的速度要快于从内存读取 Load1,因此作了这种重排。
  • StoreLoad,操做序列 Store1, StoreLoad, Load2,用于保证 Store1 写出的数据被其它 CPU 看到后才能读取 Load2 的数据到缓存。若是 Store1 和 Load2 操做的是同一个地址,StoreLoad Barrier 须要保证 Load2 不能读 Store Buffer 内的数据,得是从内存上拉取到的某个别的 CPU 修改过的值。StoreLoad 通常会认为是最重的 Barrier 也是能实现其它全部 Barrier 功能的 Barrier。

对上面四种 Barrier 解释最好的是来自这里:jdk/MemoryBarriers.java at 6bab0f539fba8fb441697846347597b4a0ade428 · openjdk/jdk · GitHub,感受比 JSR-133 Cookbook 里的还要细一点。bash

为何这一堆 Barrier 里 StoreLoad 最重?架构

所谓的重实际就是跟内存交互次数,交互越多延迟越大,也就是越重。StoreStoreLoadLoad 两个都不提了,由于它俩要么只限制读,要么只限制写,也即只有一次内存交互。只有 LoadStoreStoreLoad 看上去有可能对读写都有限制。但 LoadStore 里实际限制的更多的是读,即 Load 数据进来,它并不对最后的 Store 存出去数据的可见性有要求,只是说 Store 不能重排到 Load 以前。而反观 StoreLoad,它是说不能让 Load 重排到 Store 以前,这么一来得要求在 Load 操做前刷写 Store Buffer 到内存。不去刷 Store Buffer 的话,就可能致使先执行了读取操做,以后再刷 Store Buffer 致使写操做实际被重排到了读以后。而数据一旦刷写出去,别的 CPU 就能看到,看到以后可能就会修改下一步 Load 操做的内存致使 Load 操做的内存所在 Cache Line 无效。若是容许 Load 操做从一个可能被 Invalidate 的 Cache Line 里读数据,则表示 Load 从实际意义上来讲被重排到了 Store 以前,由于这个数据多是 Store 前就在 Cache 中的,至关于读操做提早了。为了不这种事发生,Store 完成后必定要去处理 Invalidate Queue,去判断本身 Load 操做的内存所在 Cache Line 是否被设置为无效。这么一来为了知足 StoreLoad 的要求,一方面要刷 Store Buffer,一方面要处理 Invalidate Queue,则最差状况下会有两次内存操做,读写分别一次,因此它最重。并发

StoreLoad 为何能实现其它 Barrier 的功能?

这个也是从前一个问题结果能看出来的。StoreLoad 由于对读写操做均有要求,因此它能实现其它 Barrier 的功能。其它 Barrier 都是只对读写之中的一个方面有要求。

不过这四个 Barrier 只是 Java 为了跨平台而设计出来的,实际上根据 CPU 的不一样,对应 CPU 平台上的 JVM 可能能够优化掉一些 Barrier。好比不少 CPU 在读写同一个变量的时候能保证它连续操做的顺序性,那就不用加 Barrier 了。好比 Load x; Load x.field 读 x 再读 x 下面某个 field,若是访问同一个内存 CPU 能保证顺序性,两次读取之间的 Barrier 就再也不须要了,根据字节码编译获得的汇编指令中,原本应该插入 Barrier 的地方会被替换为 nop,即空操做。在 x86 上,实际只有 StoreLoad 这一个 Barrier 是有效的,x86 上没有 Invalidate Queue,每次 Store 数据又都会去 Store Buffer 排队,因此 StoreStoreLoadLoad 都不须要。x86 又能保证 Store 操做都会走 Store Buffer 异步刷写,Store 不会被重排到 Load 以前,LoadStore 也是不须要的。只剩下一个 StoreLoad Barrier 在 x86 平台的 JVM 上被使用。

x86 上怎么使用 Barrier 的说明能够在 openjdk 的代码中看到,在这里src/hotspot/cpu/x86/assembler_x86.hpp。能够看到 x86 下使用的是 lock 来实现 StoreLoad,而且只有 StoreLoad 有效果。在这个代码注释中还大体介绍了使用 lock 的缘由。

volatile

JVM 上对 Barrier 的一个主要应用是在 volatile 关键字的实现上。对这个关键字的实现 Oracle 有这么一段描述:

Using volatile variables reduces the risk of memory consistency errors, because any write to a volatile variable establishes a happens-before relationship with subsequent reads of that same variable. This means that changes to a volatile variable are always visible to other threads. What's more, it also means that when a thread reads a volatile variable, it sees not just the latest change to the volatile, but also the side effects of the code that led up the change.

来自 Oracle 对 Atomic Access 的说明:Atomic Access。大体上就是说被 volatile 标记的变量须要维护两个特性:

  • 可见性,每次读 volatile 变量总能读到它最新值,即最后一个对它的写入操做,无论这个写入是否是当前线程完成的。
  • 禁止指令重排,也即维护 happens-before 关系,对 volatile 变量的写入不能重排到写入以前的操做以前,从而保证别的线程看到写入值后就能知道写入以前的操做都已经发生过;对 volatile 的读取操做必定不能被重排到后续操做以后,好比我须要读 volatile后根据读到的值作一些事情,作这些事情若是重排到了读 volatile 以前,则至关于没有知足读 volatile 须要读到最新值的要求,由于后续这些事情是根据一个旧 volatile 值作的。

须要看到两个事情,一个是禁止指令重排不是禁止全部的重排,只是 volatile 写入不能向前排,读取不能向后排。别的重排仍是会容许。另外一个是禁止指令重排实际也是为了去知足可见性而附带产生的。因此 volatile 对上述两个特性的维护就能靠 Barrier 来实现。

假设约定 Normal Load, Normal Store 对应的是对普通引用的修改。比如有 int a = 1;a = 2; 就是 Normal Store,int b = a; 就有一次对 a 的 Normal Load。若是变量带着 volatile 修饰,那对应的读取和写入操做就是 Volatile Load 或者 Volatile Store。volatile 对代码生成的字节码自己没有影响,即 Java Method 生成的字节码不管里面操做的变量是否是 volatile 声明的,生成的字节码都是同样的。volatile 在字节码层面影响的是 Class 内 Field 的 access_flags(参看 Java 11 The Java Virtual Machine Specification 的 4.5 节),能够理解为当看到一个成员变量被声明为 volatile,Java 编译器就在这个成员变量上打个标记记录它是 volatile 的。JVM 在将字节码编译为汇编时,若是遇见好比 getfield, putfield 这些字节码,而且发现操做的是带着 volatile 标记的成员变量,就会在汇编指令中根据 JMM 要求插入对应的 Barrier。

根据 volatile 语义,咱们依次看下面操做次序该用什么 Barrier,须要说明的是这里先后两个操做须要操做不一样的变量:

  • Normal Store, Volatile Store。即先写一个普通变量,再写一个带 volatile 的变量。这种很明显是得用 StoreStore Barrier。
  • Volatile Store, Volatile Store。也明显是 StoreStore,由于第二次修改被别的 CPU 看到时须要保证此次写入以前的写入都能被看到。
  • Nolmal Load, Volatile Store。得用 LoadStore,避免 Store 操做重排到 Load 以前。
  • Volatile Load, Volatile Store。得用 LoadStore,缘由同上。

上面四种状况要用 Barrier 的缘由统一来讲就是前面 Oracle 对 Atomic Access 的说明,写一个 volatile 的变量被别的 CPU 看到时,须要保证写这个变量操做以前的操做都能完成,无论前一个操做是读仍是写,操做的是 volatile 变量仍是不是。若是 Store 操做作了重排,排到了前一个操做以前,就会违反这个约定。因此 volatile 变量操做是在 Store 操做前面加 Barrier,而 Store 后若是是 Normal 变量就不用 Barrier 了,重不重排都无所谓:

  • Volatile Store, Normal Load
  • Volatile Store, Normal Store

对于 volatile 变量的读操做,为了知足前面提到 volatile 的两个特性,为了不后一个操做重排到读 volatile 操做以前,因此对 volatile 的读操做都是在读后面加 Barrier:

  • Volatile Load, Volatile Load。得用 LoadLoad。
  • Volatile Load, Normal Load。得用 LoadLoad。
  • Volatile Load, Normal Store。得用 LoadStore。

而若是后一个操做是 Load,则不须要再用 Barrier,能随意重排:

  • Normal Store, Volatile Load。
  • Normal Load, Volatile Load。

最后还有个特别的,前一个操做是 Volatile Store,后一个操做是 Volatile Load:

  • Volatile Store, Volatile Load。得用 StoreLoad。由于前一个 Store 以前的操做可能致使后一个 Load 的变量发生变化,后一个 Load 操做须要能看到这个变化。

还剩下四个 Normal 的操做,都是随意重排,没影响:

  • Normal Store, Normal Load
  • Normal Load, Normal Load
  • Normal Store, Normal Store
  • Normal Load, Normal Store

这些使用方式和 Java 下具体操做的对应表以下:

图中 Monitor Enter 和 Monitor Exit 分别对应着进出 synchronized 块。Monitor Ender 和 Volatile Load 对应,使用 Barrier 的方式相同。Monitor Exit 和 Volatile Store 对应,使用 Barrier 的方式相同。

总结一下这个图,记忆使用 Barrier 的方法很是简单,只要是写了 volatile 变量,为了保证对这个变量的写操做被其它 CPU 看到时,这个写操做以前发生的事情也都能被别的 CPU 看到,那就须要在写 volatile 以前加入 Barrier。避免写操做被向前重排致使 volatile 变量已经写入了被别的 CPU 看到了但它前面写入过,读过的变量却没有被别的 CPU 感知到。写入变量被别的 CPU 感知到好说,这里读变量怎么可能被别的 CPU 感知到呢?主要是

在读方面,只要是读了 volatile 变量,为了保证后续基于此次读操做而执行的操做能真的根据读到的最新值作接下来的事情,须要在读操做以后加 Barrier。

在此以外加一个特殊的 Volatile Store, Volatile Load,为了保证后一个读取能看到由于前一次写入致使的变化,因此须要加入 StoreLoad Barrier。

JMM 说明中,除了上面表中讲的 volatile 变量相关的使用 Barrier 地方以外,还有个特殊地方也会用到 Barrier,是 final 修饰符。在修改 final 变量和修改别的共享变量之间,要有一个 StoreStore Barrier。例如 x.finalField = v; StoreStore; sharedRef = x;下面是一组操做举例,看具体什么样的变量被读取、写入的时候须要使用 Barrier。

最后能够看一下 JSR-133 Cookbook 里给的例子,大概感觉一下操做各类类型变量时候 Barrier 是怎么加的:

volatile 的可见性维护

总结来讲,volatile 可见性包括两个方面:

  1. 写入的 volatile 变量在写完以后能被别的 CPU 在下一次读取中读取到;
  2. 写入 volatile 变量以前的操做在别的 CPU 看到 volatile 的最新值后必定也能被看到;

对于第一个方面,主要经过:

  1. 读取 volatile 变量不能使用寄存器,每次读取都要去内存拿
  2. 禁止读 volatile 变量后续操做被重排到读 volatile 以前

对于第二个方面,主要是经过写 volatile 变量时的 Barrier 保证写 volatile 以前的操做先于写 volatile 变量以前发生。

最后还一个特殊的,若是能用到 StoreLoad Barrier,写 volatile 后通常会触发 Store Buffer 的刷写,因此写操做能「当即」被别的 CPU 看到。

通常提到 volatile 可见性怎么实现,最常听到的解释是「写入数据以后加一个写 Barrier 去刷缓存到主存,读数据以前加入 Barrier 去强制从主存读」。

从前面对 JMM 的介绍能看到,至少从 JMM 的角度来讲,这个说法是不够准确的。一方面 Barrier 按说加在写 volatile 变量以前,不应以后加 Barrier。而读 volatile 是在以后会加 Barrier,而不在以前。另外一方面关于 「刷缓存」的表述也不够准确,即便是 StoreLoad Barrier 刷的也是 Store Buffer 到缓存里,而不是缓存向主存去刷。若是待写入的目标内存在当前 CPU Cache,即便触发 Store Buffer 刷写也是写数据到 Cache,并不会触发 Cache 的 Writeback 即向内存作同步的事情,同步主存也没有意义由于别的 CPU 并不必定关心这个值;同理,即便读 volatile 变量后有 Barrier 的存在,若是目标内存在当前 CPU Cache 且处于 Valid 状态,那读取操做就当即从 Cache 读,并不会真的再去内存拉一遍数据。

须要补充的是不管是volatile 仍是普通变量在读写操做自己方面彻底是同样的,即读写操做都交给 Cache,Cache 经过 MESI 及其变种协议去作缓存一致性维护。这两种变量的区别就只在于 Barrier 的使用上。

volatile 读取操做是 Free 的吗

在 x86 下由于除了 StoreLoad 以外其它 Barrier 都是空操做,可是读 volatile 变量并非彻底无开销,一方面 Java 的编译器仍是会遵守 JMM 要求在本该加入 Barrier 的汇编指令处填入 nop,这会阻碍 Java 编译器的一些优化措施。好比原本能进行指令重排的不敢进行指令重排等。另外由于访问的变量被声明为 volatile,每次读取它都得从内存( 或 Cache ) 要,而不能把 volatile 变量放入寄存器反复使用。这也下降了访问变量的性能。

理想状况下对 volatile 字段的使用应当多读少写,而且尽可能只有一个线程进行写操做。不过读 volatile 相对读普通变量来讲也有开销存在,只是通常不是特别重。

回顾 False Sharing 里的例子

[[CPU Cache 基础]] 这篇文章内介绍了 False Sharing 的概念以及如何观察到 False Sharing 现象。其中有个关键点是为了能更好的观察到 False Sharing,得将被线程操做的变量声明为 volatile,这样 False Sharing 出现时性能降低会很是多,但若是去掉 volatile 性能降低比率就会减小,这是为何呢?

简单来讲若是没有 volatile 声明,也即没有 Barrier 存在,每次对变量进行修改若是当前变量所在内存的 Cache Line 不在当前 CPU,那就将修改的操做放在 Store Buffer 内等待目标 Cache Line 加载后再实际执行写入操做,这至关于写入操做在 Store Buffer 内作了积累,好比 a++ 操做不是每次执行都会向 Cache 里执行加一,而是在 Cache 加载后直接执行好比加 10,加 100,从而将一批加一操做合并成一次 Cache Line 写入操做。而有了 volatile 声明,有了 Barrier,为了保证写入数据的可见性,就会引入等待 Store Buffer 刷写 Cache Line 的开销。当目标 Cache Line 还未加载入当前 CPU 的 Cache,写数据先写 Store Buffer,但看到例如 StoreLoad Barrier 后须要等待 Store Buffer 的刷写才能继续执行下一条指令。仍是拿 a++ 来讲,每次加一操做再也不能积累,而是必须等着 Cache Line 加载,执行完 Store Buffer 刷写后才能继续下一个写入,这就放大了 Cache Miss 时的影响,因此出现 False Sharing 时 Cache Line 在多个 CPU 之间来回跳转,在被修改的变量有了 volatile 声明后会执行的更慢。

再进一步说,我是在我本机作测试,个人机器是 x86 架构的,在个人机器上实际只有 StoreLoad Barrier 会真的起做用。咱们去 open jdk 的代码里看看 StoreLoad Barrier 是怎么加上的。

先看这里,JSR-133 Cookbook 里定义了一堆 Barrier,但 JVM 虚拟机上实际还会定义更多一些 Barrier 在 src/hotspot/share/runtime/orderAccess.hpp

每一个不一样的系统或 CPU 架构会使用不一样的 orderAccess 的实现,好比 linux x86 的在 src/hotspot/os_cpu/linux_x86/orderAccess_linux_x86.hpp,BSD x86 和 Linux x86 的相似在 src/hotspot/os_cpu/bsd_x86/orderAccess_bsd_x86.hpp,都是这样定义的:

inline void OrderAccess::loadload()   { compiler_barrier(); }
inline void OrderAccess::storestore() { compiler_barrier(); }
inline void OrderAccess::loadstore()  { compiler_barrier(); }
inline void OrderAccess::storeload()  { fence();            }

inline void OrderAccess::acquire()    { compiler_barrier(); }
inline void OrderAccess::release()    { compiler_barrier(); }

inline void OrderAccess::fence() {
   // always use locked addl since mfence is sometimes expensive
#ifdef AMD64
  __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
  __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  compiler_barrier();
}
复制代码

compiler_barrier() 只是为了避免作指令重排,可是对应的是空操做。看到上面只有 StoreLoad 是实际有效的,对应的是 fence(),看到 fence() 的实现是用 lock。为啥用 lock 在前面贴的 assembler_x86 的注释里有说明。

以后 volatile 变量在每次修改后,都须要使用 StoreLoad Barrier,在解释执行字节码的代码里能看到。src/hotspot/share/interpreter/bytecodeInterpreter.cpp,看到是执行 putfield 的时候若是操做的是 volatile 变量,就在写完以后加一个 StoreLoad Barrier。咱们还能找到 MonitorExit 至关于对 volatile 的写入,在 JSR-133 Cookbook 里有说过,在 openjdk 的代码里也能找到证据在 src/hotspot/share/runtime/objectMonitor.cpp

JSR-133 Cookbook 还提到 final 字段在初始化后须要有 StoreStore Barrier,在 src/hotspot/share/interpreter/bytecodeInterpreter.cpp 也能找到。

这里问题又来了,按 JSR-133 Cookbook 上给的图,连续两次 volatile 变量写入中间不应用的是 StoreStore 吗,从上面代码看用的怎么是 StoreLoad。从 JSR-133 Cookbook 上给的 StoreLoad是说 Store1; StoreLoad; Load2 含义是 Barrier 后面的全部读取操做都不能重排在 Store1 前面,并非仅指紧跟着 Store1 后面的那次读,而是无论隔多远只要有读取都不能作重排。因此我理解拿 volatile 修饰的变量来讲,写完 volatile 以后,程序总有某个位置会去读这个 volatile 变量,因此写完 volatile 变量后必定总对应着 StoreLoad Barrier,只是理论上存在好比只写 volatile 变量但历来不读它,这时候才可能产生 StoreStore Barrier。固然这个只是我从 JDK 代码上和实际测试中获得的结论。

怎么观察到上面说的内容是否是真的呢?咱们须要把 JDK 编码结果打印出来。能够参考这篇文章。简单来讲有两个关键点:

  • 启动 Java 程序时候带着这些参数:-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
  • 须要想办法下载或编译出来 hsdis,放在 JAVA_HOMEjre/lib 下面

若是缺乏 hsdis 则会在启动程序时候看到:

Could not load hsdis-amd64.dylib; library not loadable; PrintAssembly is disabled
复制代码

以后咱们去打印以前测试 False Sharing 例子中代码编译出来的结果,能够看到汇编指令中,每次执行完写 volatilevalueA 或者 valueB 后面都跟着 lock 指令,即便 JIT 介入后依然如此,汇编指令大体上相似于:

0x0000000110f9b180: lock addl $0x0,(%rsp)     ;*putfield valueA
                                                ; - cn.leancloud.filter.service.SomeClassBench::testA@2 (line 22)
复制代码

内存屏障在 JVM 的其它应用

Atomic 的 LazySet

跟 Barrier 相关的还一个有意思的,是 Atomic 下的 LazySet 操做。拿最多见的 AtomicInteger 为例,里面的状态 value 是个 volatileint,普通的 set 就是将这个状态修改成目标值,修改后由于有 Barrier 的关系会让其它 CPU 可见。而 lazySetset 对比是这样:

public final void set(int newValue) {
    value = newValue;
}
public final void lazySet(int newValue) {
    unsafe.putOrderedInt(this, valueOffset, newValue);
}
复制代码

对于 unsafe.putOrderedInt() 的内容 Java 彻底没给出解释,但从添加 lazySet()这个功能的地方: Bug ID: JDK-6275329 Add lazySet methods to atomic classes,能看出来其做用是在写入 volatile 状态前增长 StoreStore Barrier。它只保证本次写入不会重排到前面写入以前,但本次写入何时能刷写到内存是不作要求的,从而是一次轻量级的写入操做,在特定场景能优化性能。

ConcurrentLinkedQueue 下的黑科技

简单介绍一下这个黑科技。好比如今有 a b c d 四个 volatile 变量,若是无脑执行:

a = 1;
b = 2;
c = 3;
d = 4;
复制代码

会在每一个语句中间加上 Barrier。直接上面这样写可能还好,都是 StoreStore 的 Barrier,但若是写 volatile 以后又有一些读 volatile 操做,可能 Barrier 就会提高至最重的 StoreLoad Barrier,开销就会很大。而若是对开始的 a b c 写入都是用写普通变量的方式写入,只对最后的 d 用 volatile 方式更新,即只在 d = 4前带上写 Barrier,保证 d = 4 被其它 CPU 看见时,a、b、c 的值也能被别的 CPU 看见。这么一来就能减小 Barrier 的数量,提升性能。

JVM 里上一节介绍的 unsafe 下还有个叫 putObject 的方法,用来将一个 volatile 变量以普通变量方式更新,即不使用 Barrier。用这个 putObject 就能作到上面提到的优化。

ConcurrentLinkedQueue 是 Java 标准库提供的无锁队列,它里面就用到了这个黑科技。由于是链表,因此里面有个叫 Node 的类用来存放数据,Node 连起来就构成链表。Node 内有个被 volatile 修饰的变量指向 Node 存放的数据。Node 的部分代码以下:

private static class Node<E> {
    volatile E item;
    volatile Node<E> next;
    Node(E item) {
        UNSAFE.putObject(this, itemOffset, item);
    }
    ....
}
复制代码

由于 Node 被构造出来后它得经过 cas 操做队尾 Nodenext 引用接入链表,接入成功以后才须要被其它 CPU 看到,在 Node 刚构造出来的时候,Node 内的 item 实际不会被任何别的线程访问,因此看到 Node 的构造函数能够直接用 putObject 更新 item,等后续 cas 操做队列队尾 Nodenext 时候再以 volatile 方式更新 next,从而带上 Barrier,更新完成后 next 的更新包括 Nodeitem 的更新就都被别的 CPU 看到了。从而减小操做 volatile 变量的开销。

相关文章
相关标签/搜索