内存屏障是特定于硬件的
本文不想针对所有内存屏障做一综述。这将是一件不朽的功绩。但是,重要的是认识到这些指令在不同的硬件体系中迥异。下面的指令是连续写操作在多处理 Intel Xeon硬件上编译的结果。本文后面的所有汇编指令除非特殊声明否则都出自于Intel Xeon。
- 1 0x03f8340c: push %ebp ;...55
- 2 0x03f8340d: sub $0x8,%esp ;...81ec0800 0000
- 3 0x03f83413: mov $0x14c,%edi ;...bf4c0100 00
- 4 0x03f83418: movb $0x1,-0x505a72f0(%edi) ;...c687108d a5af01
- 5 0x03f8341f: mfence ;...0faef0
- 6 0x03f83422: mov $0x148,%ebp ;...bd480100 00
- 7 0x03f83427: mov $0x14d,%edx ;...ba4d0100 00
- 8 0x03f8342c: movsbl -0x505a72f0(%edx),%ebx ;...0fbe9a10 8da5af
- 9 0x03f83433: test %ebx,%ebx ;...85db
- 10 0x03f83435: jne 0x03f83460 ;...7529
- 11 0x03f83437: movl $0x1,-0x505a72f0(%ebp) ;...c785108d a5af01
- 12 0x03f83441: movb $0x0,-0x505a72f0(%edi) ;...c687108d a5af00
- 13 0x03f83448: mfence ;...0faef0
- 14 0x03f8344b: add $0x8,%esp ;...83c408
- 15 0x03f8344e: pop %ebp ;...5d
我们可以看到x86 Xeon在第11、12行执行两次volatile写操作。第二次写操作后面紧跟着mfence操作——显式的双向内存屏障,下面的连续写操作基于SPARC。
- 1 0xfb8ecc84: ldub [ %l1 + 0x155 ], %l3 ;...e60c6155
- 2 0xfb8ecc88: cmp %l3, 0 ;...80a4e000
- 3 0xfb8ecc8c: bne,pn %icc, 0xfb8eccb0 ;...12400009
- 4 0xfb8ecc90: nop ;...01000000
- 5 0xfb8ecc94: st %l0, [ %l1 + 0x150 ] ;...e0246150
- 6 0xfb8ecc98: clrb [ %l1 + 0x154 ] ;...c02c6154
- 7 0xfb8ecc9c: membar #StoreLoad ;...8143e002
- 8 0xfb8ecca0: sethi %hi(0xff3fc000), %l0 ;...213fcff0
- 9 0xfb8ecca4: ld [ %l0 ], %g0 ;...c0042000
- 10 0xfb8ecca8: ret ;...81c7e008
- 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类这个例子。
- class Counter{
-
- static int counter = 0;
-
- public static void main(String[] _){
- for(int i = 0; i < 100000; i++)
- inc();
- }
-
- static synchronized void inc(){ counter += 1; }
-
- }
Counter类执行了一个典型的读-修改-写的操作。静态counter字段不是volatile的,因为所有三个操作必须要原子可见的。因此,inc 方法是synchronized修饰的。我们可以采用下面的命令编译Counter类并查看生成的汇编指令。Java内存模型确保了synchronized区域的退出和volatile内存操作都是相同的可见性,因此我们应该预料到会有另一个内存屏障。
- $ java -XX:+UnlockDiagnosticVMOptions -XX:PrintAssemblyOptions=hsdis-print-bytes
- -XX:-UseBiasedLocking -XX:CompileCommand=print,Counter.inc Counter
- 1 0x04d5eda7: push %ebp ;...55
- 2 0x04d5eda8: mov %esp,%ebp ;...8bec
- 3 0x04d5edaa: sub $0x28,%esp ;...83ec28
- 4 0x04d5edad: mov $0x95ba5408,%esi ;...be0854ba 95
- 5 0x04d5edb2: lea 0x10(%esp),%edi ;...8d7c2410
- 6 0x04d5edb6: mov %esi,0x4(%edi) ;...897704
- 7 0x04d5edb9: mov (%esi),%eax ;...8b06
- 8 0x04d5edbb: or $0x1,%eax ;...83c801
- 9 0x04d5edbe: mov %eax,(%edi) ;...8907
- 10 0x04d5edc0: lock cmpxchg %edi,(%esi) ;...f00fb13e
- 11 0x04d5edc4: je 0x04d5edda ;...0f841000 0000
- 12 0x04d5edca: sub %esp,%eax ;...2bc4
- 13 0x04d5edcc: and $0xfffff003,%eax ;...81e003f0 ffff
- 14 0x04d5edd2: mov %eax,(%edi) ;...8907
- 15 0x04d5edd4: jne 0x04d5ee11 ;...0f853700 0000
- 16 0x04d5edda: mov $0x95ba52b8,%eax ;...b8b852ba 95
- 17 0x04d5eddf: mov 0x148(%eax),%esi ;...8bb04801 0000
- 18 0x04d5ede5: inc %esi ;...46
- 19 0x04d5ede6: mov %esi,0x148(%eax) ;...89b04801 0000
- 20 0x04d5edec: lea 0x10(%esp),%eax ;...8d442410
- 21 0x04d5edf0: mov (%eax),%esi ;...8b30
- 22 0x04d5edf2: test %esi,%esi ;...85f6
- 23 0x04d5edf4: je 0x04d5ee07 ;...0f840d00 0000
- 24 0x04d5edfa: mov 0x4(%eax),%edi ;...8b7804
- 25 0x04d5edfd: lock cmpxchg %esi,(%edi) ;...f00fb137
- 26 0x04d5ee01: jne 0x04d5ee1f ;...0f851800 0000
- 27 0x04d5ee07: mov %ebp,%esp ;...8be5
- 28 0x04d5ee09: pop %ebp ;...5d
不出意外,synchronized生成的指令数量比volatile多。第18行做了一次增操作,但是JVM没有显式插入内存屏障。相反,JVM通过在 第10行和第25行cmpxchg的lock前缀一石二鸟。cmpxchg的语义超越了本文的范畴。
lock cmpxchg不仅原子性执行写操作,也会刷新等待的读写操作。写操作现在将在所有后续内存操作之前完成。如果我们通过java.util.concurrent.atomic.AtomicInteger 重构和运行Counter,将看到同样的手段。
- import java.util.concurrent.atomic.AtomicInteger;
-
- class Counter{
-
- static AtomicInteger counter = new AtomicInteger(0);
-
- public static void main(String[] args){
- for(int i = 0; i < 1000000; i++)
- counter.incrementAndGet();
- }
-
- }
-
- $ java -XX:+UnlockDiagnosticVMOptions -XX:PrintAssemblyOptions=hsdis-print-bytes
- -XX:CompileCommand=print,*AtomicInteger.incrementAndGet Counter
- 1 0x024451f7: push %ebp ;...55
- 2 0x024451f8: mov %esp,%ebp ;...8bec
- 3 0x024451fa: sub $0x38,%esp ;...83ec38
- 4 0x024451fd: jmp 0x0244520a ;...e9080000 00
- 5 0x02445202: xchg %ax,%ax ;...6690
- 6 0x02445204: test %eax,0xb771e100 ;...850500e1 71b7
- 7 0x0244520a: mov 0x8(%ecx),%eax ;...8b4108
- 8 0x0244520d: mov %eax,%esi ;...8bf0
- 9 0x0244520f: inc %esi ;...46
- 10 0x02445210: mov $0x9a3f03d0,%edi ;...bfd0033f 9a
- 11 0x02445215: mov 0x160(%edi),%edi ;...8bbf6001 0000
- 12 0x0244521b: mov %ecx,%edi ;...8bf9
- 13 0x0244521d: add $0x8,%edi ;...83c708
- 14 0x02445220: lock cmpxchg %esi,(%edi) ;...f00fb137
- 15 0x02445224: mov $0x1,%eax ;...b8010000 00
- 16 0x02445229: je 0x02445234 ;...0f840500 0000
- 17 0x0244522f: mov $0x0,%eax ;...b8000000 00
- 18 0x02445234: cmp $0x0,%eax ;...83f800
- 19 0x02445237: je 0x02445204 ;...74cb
- 20 0x02445239: mov %esi,%eax ;...8bc6
- 21 0x0244523b: mov %ebp,%esp ;...8be5
- 22 0x0244523d: pop %ebp ;...5d
我们又一次在第14行看到了带有lock前缀的写操作。这确保了变量的新值(写操作)会在其他所有后续内存操作之前完成。
(常州java培训)