首页 > PHP资讯 > JAVA培训 > 深入Java底层:内存屏障与JVM并发详解(2)

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

JAVA培训

内存屏障是特定于硬件的

本文不想针对所有内存屏障做一综述。这将是一件不朽的功绩。但是,重要的是认识到这些指令在不同的硬件体系中迥异。下面的指令是连续写操作在多处理 Intel Xeon硬件上编译的结果。本文后面的所有汇编指令除非特殊声明否则都出自于Intel Xeon。


 
  1. 1  0x03f8340c: push   %ebp               ;...55  
  2.  2  0x03f8340d: sub    $0x8,%esp          ;...81ec0800 0000  
  3.  3  0x03f83413: mov    $0x14c,%edi        ;...bf4c0100 00  
  4.  4  0x03f83418: movb   $0x1,-0x505a72f0(%edi)  ;...c687108d a5af01  
  5.  5  0x03f8341f: mfence                    ;...0faef0  
  6.  6  0x03f83422: mov    $0x148,%ebp        ;...bd480100 00  
  7.  7  0x03f83427: mov    $0x14d,%edx        ;...ba4d0100 00  
  8.  8  0x03f8342c: movsbl -0x505a72f0(%edx),%ebx  ;...0fbe9a10 8da5af  
  9.  9  0x03f83433: test   %ebx,%ebx          ;...85db  
  10. 10  0x03f83435: jne    0x03f83460         ;...7529  
  11. 11  0x03f83437: movl   $0x1,-0x505a72f0(%ebp)  ;...c785108d a5af01  
  12. 12  0x03f83441: movb   $0x0,-0x505a72f0(%edi)  ;...c687108d a5af00  
  13. 13  0x03f83448: mfence                    ;...0faef0  
  14. 14  0x03f8344b: add    $0x8,%esp          ;...83c408  
  15. 15  0x03f8344e: pop    %ebp               ;...5d 

我们可以看到x86 Xeon在第11、12行执行两次volatile写操作。第二次写操作后面紧跟着mfence操作——显式的双向内存屏障,下面的连续写操作基于SPARC。


 
  1.  1 0xfb8ecc84: ldub  [ %l1 + 0x155 ], %l3  ;...e60c6155  
  2.  2 0xfb8ecc88: cmp  %l3, 0               ;...80a4e000  
  3.  3 0xfb8ecc8c: bne,pn   %icc, 0xfb8eccb0  ;...12400009  
  4.  4 0xfb8ecc90: nop                       ;...01000000  
  5.  5 0xfb8ecc94: st  %l0, [ %l1 + 0x150 ]  ;...e0246150  
  6.  6 0xfb8ecc98: clrb  [ %l1 + 0x154 ]     ;...c02c6154  
  7.  7 0xfb8ecc9c: membar  #StoreLoad        ;...8143e002  
  8.  8 0xfb8ecca0: sethi  %hi(0xff3fc000), %l0  ;...213fcff0  
  9.  9 0xfb8ecca4: ld  [ %l0 ], %g0          ;...c0042000  
  10. 10 0xfb8ecca8: ret                       ;...81c7e008  
  11. 11 0xfb8eccac: restore                   ;...81e80000 

我们看到在第五、六行存在两次volatile写操作。第二次写操作后面是一个membar指令——显式的双向内存屏障。x86和SPARC的指令流与Itanium的指令流存在一个重要区别。JVM在x86和SPARC上通过内存屏障跟踪连续写操作,但是在两次写操作之间没有放置内存屏障。

另一方面,Itanium的指令流在两次写操作之间存在内存屏障。为何JVM在不同的硬件架构之间表现不一?因为硬件架构都有自己的内 存模型,每一个内存模型有一套一致性保障。某些内存模型,如x86和SPARC等,拥有强大的一致性保障。另一些内存模型,如Itanium、 PowerPC和Alpha,是一种弱保障。

例如,x86和SPARC不会重新排序连续写操作——也就没有必要放置内存屏障。Itanium、 PowerPC和Alpha将重新排序连续写操作——因此JVM必须在两者之间放置内存屏障。JVM使用内存屏障减少Java内存模型和硬件内存模型之间的距离。

隐式内存屏障

显式屏障指令不是序列化内存操作的唯一方式。让我们再看一看Counter类这个例子。


 
  1. class Counter{  
  2.  
  3.     static int counter = 0;  
  4.  
  5.     public static void main(String[] _){  
  6.         for(int i = 0; i < 100000; i++)  
  7.             inc();  
  8.     }  
  9.  
  10.     static synchronized void inc(){ counter += 1; }  
  11.  

Counter类执行了一个典型的读-修改-写的操作。静态counter字段不是volatile的,因为所有三个操作必须要原子可见的。因此,inc 方法是synchronized修饰的。我们可以采用下面的命令编译Counter类并查看生成的汇编指令。Java内存模型确保了synchronized区域的退出和volatile内存操作都是相同的可见性,因此我们应该预料到会有另一个内存屏障。


 
  1. $ java -XX:+UnlockDiagnosticVMOptions -XX:PrintAssemblyOptions=hsdis-print-bytes   
  2. -XX:-UseBiasedLocking -XX:CompileCommand=print,Counter.inc Counter   
  3.  1  0x04d5eda7: push   %ebp               ;...55  
  4.  2  0x04d5eda8: mov    %esp,%ebp          ;...8bec  
  5.  3  0x04d5edaa: sub    $0x28,%esp         ;...83ec28  
  6.  4  0x04d5edad: mov    $0x95ba5408,%esi   ;...be0854ba 95  
  7.  5  0x04d5edb2: lea    0x10(%esp),%edi    ;...8d7c2410  
  8.  6  0x04d5edb6: mov    %esi,0x4(%edi)     ;...897704  
  9.  7  0x04d5edb9: mov    (%esi),%eax        ;...8b06  
  10.  8  0x04d5edbb: or     $0x1,%eax          ;...83c801  
  11.  9  0x04d5edbe: mov    %eax,(%edi)        ;...8907  
  12. 10  0x04d5edc0: lock cmpxchg %edi,(%esi)  ;...f00fb13e  
  13. 11  0x04d5edc4: je     0x04d5edda         ;...0f841000 0000  
  14. 12  0x04d5edca: sub    %esp,%eax          ;...2bc4  
  15. 13  0x04d5edcc: and    $0xfffff003,%eax   ;...81e003f0 ffff  
  16. 14  0x04d5edd2: mov    %eax,(%edi)        ;...8907  
  17. 15  0x04d5edd4: jne    0x04d5ee11         ;...0f853700 0000  
  18. 16  0x04d5edda: mov    $0x95ba52b8,%eax   ;...b8b852ba 95  
  19. 17  0x04d5eddf: mov    0x148(%eax),%esi   ;...8bb04801 0000  
  20. 18  0x04d5ede5: inc    %esi               ;...46  
  21. 19  0x04d5ede6: mov    %esi,0x148(%eax)   ;...89b04801 0000  
  22. 20  0x04d5edec: lea    0x10(%esp),%eax    ;...8d442410  
  23. 21  0x04d5edf0: mov    (%eax),%esi        ;...8b30  
  24. 22  0x04d5edf2: test   %esi,%esi          ;...85f6  
  25. 23  0x04d5edf4: je     0x04d5ee07         ;...0f840d00 0000  
  26. 24  0x04d5edfa: mov    0x4(%eax),%edi     ;...8b7804  
  27. 25  0x04d5edfd: lock cmpxchg %esi,(%edi)  ;...f00fb137  
  28. 26  0x04d5ee01: jne    0x04d5ee1f         ;...0f851800 0000  
  29. 27  0x04d5ee07: mov    %ebp,%esp          ;...8be5  
  30. 28  0x04d5ee09: pop    %ebp               ;...5d 

不出意外,synchronized生成的指令数量比volatile多。第18行做了一次增操作,但是JVM没有显式插入内存屏障。相反,JVM通过在 第10行和第25行cmpxchg的lock前缀一石二鸟。cmpxchg的语义超越了本文的范畴。

lock cmpxchg不仅原子性执行写操作,也会刷新等待的读写操作。写操作现在将在所有后续内存操作之前完成。如果我们通过java.util.concurrent.atomic.AtomicInteger 重构和运行Counter,将看到同样的手段。


 
  1.  import java.util.concurrent.atomic.AtomicInteger;  
  2.  
  3.     class Counter{  
  4.  
  5.         static AtomicInteger counter = new AtomicInteger(0);  
  6.  
  7.         public static void main(String[] args){  
  8.             for(int i = 0; i < 1000000; i++)  
  9.                 counter.incrementAndGet();  
  10.         }  
  11.  
  12.     }  
  13.  
  14. $ java -XX:+UnlockDiagnosticVMOptions -XX:PrintAssemblyOptions=hsdis-print-bytes   
  15. -XX:CompileCommand=print,*AtomicInteger.incrementAndGet Counter   
  16.  1  0x024451f7: push   %ebp               ;...55  
  17.  2  0x024451f8: mov    %esp,%ebp          ;...8bec  
  18.  3  0x024451fa: sub    $0x38,%esp         ;...83ec38  
  19.  4  0x024451fd: jmp    0x0244520a         ;...e9080000 00  
  20.  5  0x02445202: xchg   %ax,%ax            ;...6690  
  21.  6  0x02445204: test   %eax,0xb771e100    ;...850500e1 71b7  
  22.  7  0x0244520a: mov    0x8(%ecx),%eax     ;...8b4108  
  23.  8  0x0244520d: mov    %eax,%esi          ;...8bf0  
  24.  9  0x0244520f: inc    %esi               ;...46  
  25. 10  0x02445210: mov    $0x9a3f03d0,%edi   ;...bfd0033f 9a  
  26. 11  0x02445215: mov    0x160(%edi),%edi   ;...8bbf6001 0000  
  27. 12  0x0244521b: mov    %ecx,%edi          ;...8bf9  
  28. 13  0x0244521d: add    $0x8,%edi          ;...83c708  
  29. 14  0x02445220: lock cmpxchg %esi,(%edi)  ;...f00fb137  
  30. 15  0x02445224: mov    $0x1,%eax          ;...b8010000 00  
  31. 16  0x02445229: je     0x02445234         ;...0f840500 0000  
  32. 17  0x0244522f: mov    $0x0,%eax          ;...b8000000 00  
  33. 18  0x02445234: cmp    $0x0,%eax          ;...83f800  
  34. 19  0x02445237: je     0x02445204         ;...74cb  
  35. 20  0x02445239: mov    %esi,%eax          ;...8bc6  
  36. 21  0x0244523b: mov    %ebp,%esp          ;...8be5  
  37. 22  0x0244523d: pop    %ebp               ;...5d 

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

常州java培训

本文由欣才IT学院整理发布,未经许可,禁止转载。