
深入Java底层内存屏障与JVM并发详解.docx
7页深入 Java 底层:内存屏障与 JVM 并发详解内存屏障,又称内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限制本文假定读者已经充分掌握了相关概念和 Java 内存模型,不讨论并发互斥、并行机制和原子性内存屏障用来实现并发编程中称为可见性(visibility)的同样重要的作用内存屏障为何重要?对主存的一次访问一般花费硬件的数百次时钟周期处理器通过缓存(caching)能够从数量级上降低内存延迟的成本这些缓存为了性能重新排列待定内存操 作的顺序也就是说,程序的读写操作不一定会按照它要求处理器的顺序执行当数据是不可变的,同时/或者数据限制程范围内,这些优化是无害的如果把这些优化与对称多处理(symmetric multi-processing)和共享可变状态( shared mutable state)结合,那么就是一场噩梦当基于共享可变状态的内存操作被重新排序时,程序可能行为不定一个线程写入的数据可能被其他线程可见,原因是数据 写入的顺序不一致适当的放置内存屏障通过强制处理器顺序执行待定的内存操作来避免这个问题内存屏障的协调作用内存屏障不直接由 JVM 暴露,相反它们被 JVM 插入到指令序列中以维持语言层并发原语的语义。
我们研究几个简单 Java 程序的源代码和汇编指令首先快速看一下 Dekker 算法中的内存屏障该算法利用 volatile 变量协调两个线程之间的共享资源访问请不要关注该算法的出色细节哪些部分是相关的?每个线程通过发信号试图进入代码第一行的关键区域如果线程在第三行意识到冲突(两个线程都要访问),通 过 turn 变量的操作来解决在任何时刻只有一个线程可以访问关键区域1. // code run by first thread // code run by second thread2.3. 1 intentFirst = true; intentSecond = true;4. 25. 3 while (intentSecond) while (intentFirst) // volatile read6. 4 if (turn != 0) { if (turn != 1) { // volatile read7. 5 intentFirst = false; intentSecond = false;8. 6 while (turn != 0) {} while (turn != 1) {}9. 7 intentFirst = true; intentSecond = true;10. 8 } }11. 912.10 criticalSection(); criticalSection();13.1114.12 turn = 1; turn = 0; // volatile write15.13 intentFirst = false; intentSecond = false; // volatile write硬件优化可以在没有内存屏障的情况下打乱这段代码,即使编译器按照程序员的想法顺序列出所有的内存操作。
考虑第三、四行的两次顺序 volatile 读操 作每一个线程检查其他线程是否发信号想进入关键区域,然后检查轮到谁操作了考虑第 12、13 行的两次顺序写操作每一个线程把访问权释放给其他线程, 然后撤销自己访问关键区域的意图读线程应该从不期望在其他线程撤销访问意愿后观察到其他线程对 turn 变量的写操作这是个灾难但是如果这些变量没有 volatile 修饰符,这的确会发生!例如,没有 volatile 修饰符,第二个线程在第一个线程对 turn 执行写操作(倒数第二行)之前可能会观察到 第一个线程对 intentFirst(倒数第一行)的写操作关键词 volatile 避免了这种情况,因为它在对 turn 变量的写操作和对 intentFirst 变量的写 操作之间创建了一个先后关系编译器无法重新排序这些写操作,如果必要,它会利用一个内存屏障禁止处理器重排序让我们来 看看一些实现细节PrintAssembly HotSpot 选项是 JVM 的一个诊断标志,允许我们获取 JIT 编译器生成的汇编指令这需要最新的 OpenJDK 版本或者新 HotSpot update14 或者更高版本。
通过需要一个反编译插件Kenai 项目提供了用于 Solaris、Linux 和 BSD 的插件二进制文件 hsdis 是另 一款可以在 Windows 通过源码构建的插件两次顺序读操作的第一次(第三行)的汇编指令如下指令流基于 Itanium 2 多处理硬件、JDK 1.6 update 17本文的所有指令流都在左手边以行号标记相关的读操作、写操作和内存屏障指令都以粗体标记建议读者不要沉迷于每一行指令16.1 0x2000000001de819c: adds r37=597,r36;; ;...8411255417.2 0x2000000001de81a0: ld1.acq r38=[r37];; ;...0b30014a a01018.3 0x2000000001de81a6: nop.m 0x0 ;...00000002 00c019.4 0x2000000001de81ac: sxt1 r38r38=r38;; ;...0051300420.5 0x2000000001de81b0: cmp4.eq p0,p6=0,r38 ;...1100004c 863921.6 0x2000000001de81b6: nop.i 0x0 ;...00000002 000322.7 0x2000000001de81bc: br.cond.dpnt.many 0x2000000001de8220;简短的指令流其实内容丰富。
第一次 volatile 位于第二行Java 内存模型确保了 JVM 会在第二次读操作之前将第一次读操作交给处理器,也就是按照 “程序的顺序 ”--但是这单单一行指令是不够的,因为处理器仍然可以自由乱序执行这些操作为了支持 Java 内存模型的一致性,JVM 在第一次读操作上添加了注解 ld.acq,也就是“载入获取” (load acquire)通过使用 ld.acq,编译器确保第二行的读操作在接下来的读操作之前完成,问题就解决了请注意这影响了读操作,而不是写内存屏障强制读或写操作顺序限制不是单向的强制读和写操作顺序限制的内存屏障是双向的,类似于双向开的栅栏使用 ld.acq 就是单向内存屏障的例子一致性具有两面性如果一个读线程在两次读操作之间插入了内存屏障而另外一个线程没有在两次写操作之间添加内存屏障又有什么用呢?线程为了协调,必须同时 遵守这个协议,就像网络中的节点或者团队中的成员如果某个线程破坏了这个约定,那么其他所有线程的努力都白费Dekker 算法的最后两行代码的汇编指令应该插入一个内存屏障,两次 volatile 写之间23.$ java -XX:+UnlockDiagnosticVMOptions -XX:PrintAssemblyOptions=hsdis-print-bytes24.-XX:CompileCommand=print,WriterReader.write WriterReader25. 1 0x2000000001de81c0: adds r37=592,r36;; ;...0b284149 042126. 2 0x2000000001de81c6: st4.rel [r37]=r39 ;...00389560 238027. 3 0x2000000001de81cc: adds r36=596,r36;; ;...8411254428. 4 0x2000000001de81d0: st1.rel [r36]=r0 ;...09000048 a01129. 5 0x2000000001de81d6: mf ;...00000044 000030. 6 0x2000000001de81dc: nop.i 0x0;; ;...0004000031. 7 0x2000000001de81e0: mov r12=r33 ;...00600042 002132. 8 0x2000000001de81e6: mov.ret b0=r35,0x2000000001de81e033. 9 0x2000000001de81ec: mov.i ar.pfs=r34 ;...00aa022034.10 0x2000000001de81f0: mov r6=r32 ;...09300040 0021这里我们可以看到在第四行第二次写操作被注解了一个显式内存屏障。
通过使用 st.rel,即“存储释放” (store release),编译器确保第一次写操作在第二次写操作之前完成这就完成了两边的约定,因为第一次写操作在第二次写操作之前发生 st.rel 屏障是单向的--就像 ld.acq 一样但是在第五行编译器设置了一个双向内存屏障mf 指令,或者称为“ 内存栅栏”,是 Itanium 2 指令集中的完整栅栏笔者认为是多余的内存屏障是特定于硬件的本文不想针对所有内存屏障做一综述这将是一件不朽的功绩但是,重要的是认识到这些指令在不同的硬件体系中迥异下面的指令是连续写操作在多处理 Intel Xeon 硬件上编译的结果本文后面的所有汇编指令除非特殊声明否则都出自于 Intel Xeon1.1 0x03f8340c: push %ebp ;...552. 2 0x03f8340d: sub $0x8,%esp ;...81ec0800 00003. 3 0x03f83413: mov $0x14c,%edi ;...bf4c0100 004. 4 0x03f83418: movb $0x1,-0x505a72f0(%edi) ;...c687108d a5af015. 5 0x03f8341f: mfence ;...0faef06. 6 0x03f83422: mov $0x148,%ebp ;...bd480100 007. 7 0x03f83427: mov $0x14d,%edx ;...ba4d0100 008. 8 0x03f8342c: movsbl -0x505a72f0(%edx),%ebx ;...0fbe9a10 8da5af9. 9 0x03f83433: test %ebx,%ebx ;...85db10.10 0x03f83435: jne 0x03f83460 ;...752911.11 0x03f83437: movl $0x1,-0x505a72f0(%ebp) ;...c785108d a5af0112.1。












