深入Java底层:内存屏障与JVM并发详解(3)

  82.$ Java -XX:+UnlockDiagnosticVMOptions -XX:PrintAssemblyOptions=hsdis-print-bytes

  83.-XX:CompileCommand=print,*AtomicInteger.incrementAndGet Counter

  84. 1  0x024451f7: push   %ebp               ;...55

  85. 2  0x024451f8: mov    %esp,%ebp          ;...8bec

  86. 3  0x024451fa: sub    $0x38,%esp         ;...83ec38

  87. 4  0x024451fd: jmp    0x0244520a         ;...e9080000 00

  88. 5  0x02445202: xchg   %ax,%ax            ;...6690

  89. 6  0x02445204: test   %eax,0xb771e100    ;...850500e1 71b7

  90. 7  0x0244520a: mov    0x8(%ecx),%eax     ;...8b4108

  91. 8  0x0244520d: mov    %eax,%esi          ;...8bf0

  92. 9  0x0244520f: inc    %esi               ;...46

  93.10  0x02445210: mov    $0x9a3f03d0,%edi   ;...bfd0033f 9a

  94.11  0x02445215: mov    0x160(%edi),%edi   ;...8bbf6001 0000

  95.12  0x0244521b: mov    %ecx,%edi          ;...8bf9

  96.13  0x0244521d: add    $0x8,%edi          ;...83c708

  97.14  0x02445220: lock cmpxchg %esi,(%edi)  ;...f00fb137

  98.15  0x02445224: mov    $0x1,%eax          ;...b8010000 00

  99.16  0x02445229: je     0x02445234         ;...0f840500 0000

  100.17  0x0244522f: mov    $0x0,%eax          ;...b8000000 00

  101.18  0x02445234: cmp    $0x0,%eax          ;...83f800

  102.19  0x02445237: je     0x02445204         ;...74cb

  103.20  0x02445239: mov    %esi,%eax          ;...8bc6

  104.21  0x0244523b: mov    %ebp,%esp          ;...8be5

  105.22  0x0244523d: pop    %ebp               ;...5d

  我们又一次在第14行看到了带有lock前缀的写操作。这确保了变量的新值(写操作)会在其他所有后续内存操作之前完成。

  内存屏障能够避免

  JVM非常擅于消除不必要的内存屏障。通常JVM很幸运,因为硬件内存模型的一致性保障强于或者等于Java内存模型。在这种情况下,JVM只是简单地插 入一个no op语句,而不是真实的内存屏障。

  例如,x86和SPARC内存模型的一致性保障足够强壮以消除读volatile变量时所需的内存屏障。还记得在 Itanium上两次读操作之间的显式单向内存屏障吗?x86上的Dekker算法中连续volatile读操作的汇编指令之间没有任何内存屏障。x86平台上共享内存的连续读操作。

  1.1  0x03f83422: mov    $0x148,%ebp        ;...bd480100 00

  2. 2  0x03f83427: mov    $0x14d,%edx        ;...ba4d0100 00

  3. 3  0x03f8342c: movsbl -0x505a72f0(%edx),%ebx  ;...0fbe9a10 8da5af

  4. 4  0x03f83433: test   %ebx,%ebx          ;...85db

  5. 5  0x03f83435: jne    0x03f83460         ;...7529

  6. 6  0x03f83437: movl   $0x1,-0x505a72f0(%ebp)  ;...c785108d a5af01

  7. 7  0x03f83441: movb   $0x0,-0x505a72f0(%edi)  ;...c687108d a5af00

  8. 8  0x03f83448: mfence                    ;...0faef0

  9. 9  0x03f8344b: add    $0x8,%esp          ;...83c408

  10.10  0x03f8344e: pop    %ebp               ;...5d

  11.11  0x03f8344f: test   %eax,0xb78ec000    ;...850500c0 8eb7

  12.12  0x03f83455: ret                       ;...c3

  13.13  0x03f83456: nopw   0x0(%eax,%eax,1)   ;...66660f1f 840000

  14.14  0x03f83460: mov    -0x505a72f0(%ebp),%ebx  ;...8b9d108d a5af

  15.15  0x03f83466: test   %edi,0xb78ec000    ;...853d00c0 8eb7

  第三行和第十四行存在volatile读操作,而且都没有伴随内存屏障。也就是说,x86和SPARC上的volatile读操作的性能下降对于代码的优 化影响很小--指令本身和常规读操作一样。

  单向内存屏障本质上比双向屏障性能要好一些。JVM在确保单向屏障即可的情况下会避免使用双向屏障。本文的第一个例子展示了这点。Itanium平台上的 连续两次读操作被插入单向内存屏障。如果读操作插入显式双向内存屏障,程序仍然正确,但是延迟比较长。

  动态编译

  静态编译器在构建阶段决定的一切事情,在动态编译器那里都可以在运行时决定,甚至更多。更多信息意味着存在更多机会可以优化。例如,让我们看看JVM在单 处理器运行时如何对待内存屏障。以下指令流来自于通过Dekker算法实现两次连续volatile写操作的运行时编译。程序运行于 x86硬件上的单处理器模式中的VMWare工作站镜像。

  16.1  0x017b474c: push   %ebp               ;...55

  17. 2  0x017b474d: sub    $0x8,%esp          ;...81ec0800 0000

  18. 3  0x017b4753: mov    $0x14c,%edi        ;...bf4c0100 00

  19. 4  0x017b4758: movb   $0x1,-0x507572f0(%edi)  ;...c687108d 8aaf01

  20. 5  0x017b475f: mov    $0x148,%ebp        ;...bd480100 00

  21. 6  0x017b4764: mov    $0x14d,%edx        ;...ba4d0100 00

  22. 7  0x017b4769: movsbl -0x507572f0(%edx),%ebx  ;...0fbe9a10 8d8aaf

  23. 8  0x017b4770: test   %ebx,%ebx          ;...85db

  24. 9  0x017b4772: jne    0x017b4790         ;...751c

  25.10  0x017b4774: movl   $0x1,-0x507572f0(%ebp)  ;...c785108d 8aaf0111

  26.12  0x017b4785: add    $0x8,%esp          ;...83c408

  27.13  0x017b4788: pop    %ebp               ;...5d

  在单处理器系统上,JVM为所有内存屏障插入了一个no op指令,因为内存操作已经序列化了。每一个写操作(第10、11行)后面都跟着一个屏障。JVM针对原子条件式做了类似的优化。下面的指令流来自于同一 个VMWare镜像的AtomicInteger.incrementAndGet动态编译结果。

  28.1  0x036880f7: push   %ebp               ;...55

  29. 2  0x036880f8: mov    %esp,%ebp          ;...8bec

  30. 3  0x036880fa: sub    $0x38,%esp         ;...83ec38

  31. 4  0x036880fd: jmp    0x0368810a         ;...e9080000 00

  32. 5  0x03688102: xchg   %ax,%ax            ;...6690

  33. 6  0x03688104: test   %eax,0xb78b8100    ;...85050081 8bb7

  34. 7  0x0368810a: mov    0x8(%ecx),%eax     ;...8b4108

  35. 8  0x0368810d: mov    %eax,%esi          ;...8bf0

  36. 9  0x0368810f: inc    %esi               ;...46

  37.10  0x03688110: mov    $0x9a3f03d0,%edi   ;...bfd0033f 9a

  38.11  0x03688115: mov    0x160(%edi),%edi   ;...8bbf6001 0000

  39.12  0x0368811b: mov    %ecx,%edi          ;...8bf9

  40.13  0x0368811d: add    $0x8,%edi          ;...83c708

  41.14  0x03688120: cmpxchg %esi,(%edi)       ;...0fb137

  42.15  0x03688123: mov    $0x1,%eax          ;...b8010000 00

  43.16  0x03688128: je     0x03688133         ;...0f840500 0000

  44.17  0x0368812e: mov    $0x0,%eax          ;...b8000000 00

  45.18  0x03688133: cmp    $0x0,%eax          ;...83f800

  46.19  0x03688136: je     0x03688104         ;...74cc

  47.20  0x03688138: mov    %esi,%eax          ;...8bc6

  48.21  0x0368813a: mov    %ebp,%esp          ;...8be5

  49.22  0x0368813c: pop    %ebp               ;...5d

  注意第14行的cmpxchg指令。之前我们看到编译器通过lock前缀把该指令提供给处理器。由于缺少SMP,JVM决定避免这种成本--与静态编译有些不同。

  结束语

  内存屏障是多线程编程的必要装备。它们形式多样,某些是显式的,某些是隐式的。某些是双向的,某些是单向的。JVM利用这些形式在所有平台中有效地支持Java内存模型。我们希望本文能够帮助经验丰富的JVM开发人员了解一些代码在底层如何运行的知识。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wwpzyx.html