深入java底层----内存屏障与jvm并发详解

上传人:子 文档编号:46827607 上传时间:2018-06-28 格式:PDF 页数:13 大小:138.59KB
返回 下载 相关 举报
深入java底层----内存屏障与jvm并发详解_第1页
第1页 / 共13页
深入java底层----内存屏障与jvm并发详解_第2页
第2页 / 共13页
深入java底层----内存屏障与jvm并发详解_第3页
第3页 / 共13页
深入java底层----内存屏障与jvm并发详解_第4页
第4页 / 共13页
深入java底层----内存屏障与jvm并发详解_第5页
第5页 / 共13页
点击查看更多>>
资源描述

《深入java底层----内存屏障与jvm并发详解》由会员分享,可在线阅读,更多相关《深入java底层----内存屏障与jvm并发详解(13页珍藏版)》请在金锄头文库上搜索。

1、深入深入JavaJava底层底层-内存屏障与内存屏障与JVMJVM并发详解并发详解内存屏障,又称内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限制。本文 假定读者已经充分掌握了相关概念和Java内存模型,不讨论并发互斥、并行机制和原子性。 内存屏障用来实现并发编程中称为可见性(visibility)的同样重要的作用。内存屏障为何重要?对主存的一次访问一般花费硬件的数百次时钟周期。处理器通过缓存(caching)能够 从数量级上降低内存延迟的成本这些缓存为了性能重新排列待定内存操 作的顺序。也就是 说,程序的读写操作不一定会按照它要求处理器的顺序执行。当数据是不可变的,同时/或 者数据限制

2、在线程范围内,这些优化是无害的。如果把这些优化与对称多处理( symmetric multi-processing )和共享可变状态( shared mutable state)结合,那么就是一场噩梦。当基于共享可变状态的内存操作被重新排序时, 程序可能行为不定。一个线程写入的数据可能被其他线程可见,原因是数据 写入的顺序不 一致。适当的放置内存屏障通过强制处理器顺序执行待定的内存操作来避免这个问题。内存屏障的协调作用内存屏障不直接由JVM暴露, 相反它们被JVM插入到指令序列中以维持语言层并发原语的 语义。 我们研究几个简单Java程序的源代码和汇编指令。 首先快速看一下Dekker算法中的

3、内 存屏障。该算法利用volatile变量协调两个线程之间的共享资源访问。请不要关注该算法的出色细节。 哪些部分是相关的?每个线程通过发信号试图进入代码 第一行的关键区域。如果线程在第三行意识到冲突(两个线程都要访问) ,通 过turn变量的 操作来解决。在任何时刻只有一个线程可以访问关键区域。1. / code run by first thread/ code run by second thread2.3. 1intentFirst = true;intentSecond = true;4. 25. 3while (intentSecond)while (intentFirst)/ vo

4、latile read6. 4if (turn != 0) if (turn != 1) / volatile read7. 5intentFirst = false;intentSecond = false;8. 6while (turn != 0) while (turn != 1) 9. 7intentFirst = true;intentSecond = true;10. 811. 912.10criticalSection() ;criticalSection() ;13.1114.12turn = 1;turn = 0;/ volatile write15.13intentFirs

5、t = false;intentSecond = false;/ volatile write硬件优化可以在没有内存屏障的情况下打乱这段代码, 即使编译器按照程序员的想法顺 序列出所有的内存操作。考虑第三、四行的两次顺序volatile读操 作。每一个线程检查其 他线程是否发信号想进入关键区域,然后检查轮到谁操作了。考虑第12、13行的两次顺序写 操作。每一个线程把访问权释放给其他线程, 然后撤销自己访问关键区域的意图。读线程 应该从不期望在其他线程撤销访问意愿后观察到其他线程对turn变量的写操作。这是个灾 难。但是如果这些变量没有 volatile修饰符,这的确会发生!例如,没有volat

6、ile修饰符, 第二个线程在第一个线程对turn执行写操作(倒数第二行)之前可能会观察到 第一个线程 对intentFirst (倒数第一行) 的写操作。 关键词volatile避免了这种情况, 因为它在对turn 变量的写操作和对 intentFirst变量的写操作之间创建了一个先后关系。 编译器无法重新排 序这些写操作,如果必要,它会利用一个内存屏障禁止处理器重排序。让我们来 看看一些 实现细节。PrintAssembly HotSpot选项是JVM的一个诊断标志,允许我们获取JIT编译器生成的汇 编指令。这需要最新的OpenJDK版本或者新HotSpot update14或者更高版本。通

7、过需要一个 反编译插件。Kenai项目提供了用于Solaris、Linux和BSD的插件二进制文件。hsdis是另 一 款可以在Windows通过源码构建的插件。两次顺序读操作的第一次(第三行)的汇编指令如下。指令流基于Itanium 2多处理硬 件、JDK 1.6 update 17。本文的所有指令流都在左手边以行号标记。相关的读操作、写操 作和内存屏障指令都以粗体标记。建议读者不要沉迷于每一行指令。16.10x2000000001de819c:adds r37=597,r36;8411255417.20x2000000001de81a0:ld1.acq r38=r37;0b30014a a

8、01018.30x2000000001de81a6:nop.m 0x0;00000002 00c019.40x2000000001de81ac:sxt1 r38r38=r38;0051300420.50x2000000001de81b0:cmp4.eq p0,p6=0,r38;1100004c 863921.60x2000000001de81b6:nop.i 0x0;00000002 000322.70x2000000001de81bc:br.cond.dpnt.many 0x2000000001de8220;简短的指令流其实内容丰富。第一次volatile位于第二行。Java内存模型确保了J

9、VM会 在第二次读操作之前将第一次读操作交给处理器,也就是按照 “程序的顺序”-但是这单 单一行指令是不够的, 因为处理器仍然可以自由乱序执行这些操作。 为了支持Java内存模型 的一致性, JVM在第一次读操作上添加了注解ld.acq, 也就是“载入获取”( load acquire)。 通过使用ld.acq,编译器确保第二行的读操作在接下来的读操作之前完成,问题就解决了。请注意这影响了读操作,而不是写。内存屏障强制读或写操作顺序限制不是单向的。 强 制读和写操作顺序限制的内存屏障是双向的, 类似于双向开的栅栏。 使用ld.acq就是单向内 存屏障的例子。一致性具有两面性。 如果一个读线程在

10、两次读操作之间插入了内存屏障而另外一个线程 没有在两次写操作之间添加内存屏障又有什么用呢?线程为了协调,必须同时 遵守这个协 议,就像网络中的节点或者团队中的成员。如果某个线程破坏了这个约定,那么其他所有线 程的努力都白费。Dekker算法的最后两行代码的汇编指令应该插入一个内存屏障,两次 volatile写之间。23.$java-XX:+UnlockDiagnosticVMOptions -XX:PrintAssemblyOptions=hsdis-print-bytes24.-XX:CompileCommand=print,WriterReader.write WriterReader25

11、. 10x2000000001de81c0:adds r37=592,r36;0b284149 042126. 20x2000000001de81c6:st4.rel r37=r39;00389560 238027. 30x2000000001de81cc:adds r36=596,r36;8411254428. 40x2000000001de81d0:st1.rel r36=r0;09000048 a01129. 50x2000000001de81d6:mf;00000044 000030. 60x2000000001de81dc:nop.i 0x0;0004000031. 70x20000

12、00001de81e0:mov r12=r33;00600042 002132. 80x2000000001de81e6:mov.ret b0=r35,0x2000000001de81e033. 90x2000000001de81ec:mov.i ar.pfs=r34;00aa022034.100x2000000001de81f0:mov r6=r32;09300040 0021这里我们可以看到在第四行第二次写操作被注解了一个显式内存屏障。通过使用 st.rel,即“存储释放”(store release) ,编译器确保第一次写操作在第二次写操作之前 完成。这就完成了两边的约定,因为第一次写操

13、作在第二次写操作之前发生。st.rel屏障是单向的-就像ld.acq一样。但是在第五行编译器设置了一个双向内存屏 障。mf指令,或者称为“内存栅栏”,是Itanium 2指令集中的完整栅栏。笔者认为是多余 的。内存屏障是特定于硬件的本文不想针对所有内存屏障做一综述。这将是一件不朽的功绩。但是,重要的是认识到 这些指令在不同的硬件体系中迥异。下面的指令是连续写操作在多处理 Intel Xeon硬件上 编译的结果。本文后面的所有汇编指令除非特殊声明否则都出自于Intel Xeon。1.10x03f8340c: push%ebp;552. 20x03f8340d: sub$0x8,%esp;81ec

14、0800 00003. 30x03f83413: mov$0x14c,%edi;bf4c0100 004. 40x03f83418: movb$0x1,-0x505a72f0(%edi);c687108d a5af015. 50x03f8341f: mfence;0faef06. 60x03f83422: mov$0x148,%ebp;bd480100 007. 70x03f83427: mov$0x14d,%edx;ba4d0100 008. 80x03f8342c: movsbl -0x505a72f0(%edx) ,%ebx;0fbe9a10 8da5af9. 90x03f83433:

15、test%ebx,%ebx;85db10.100x03f83435: jne0x03f83460;752911.110x03f83437: movl$0x1,-0x505a72f0(%ebp);c785108d a5af0112.120x03f83441: movb$0x0,-0x505a72f0(%edi);c687108d a5af0013.130x03f83448: mfence;0faef014.140x03f8344b: add$0x8,%esp;83c40815.150x03f8344e: pop%ebp;5d我们可以看到x86 Xeon在第11、12行执行两次volatile写操

16、作。第二次写操作后面紧跟 着mfence操作-显式的双向内存屏障,下面的连续写操作基于SPARC。16. 1 0xfb8ecc84: ldub %l1 + 0x155 , %l3;e60c615517. 2 0xfb8ecc88: cmp%l3, 0;80a4e00018. 3 0xfb8ecc8c: bne,pn%icc, 0xfb8eccb0;1240000919. 4 0xfb8ecc90: nop;0100000020. 5 0xfb8ecc94: st%l0, %l1 + 0x150 ;e024615021. 6 0xfb8ecc98: clrb %l1 + 0x154 ;c02c615422. 7 0xfb8ecc9c: membar#StoreLoad;8143e00223. 8 0xfb8ecca0: sethi%hi(0xff3fc000) , %l0;213fcff024. 9 0xfb8ecc

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 生活休闲 > 科普知识

电脑版 |金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号