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开发人员了解一些代码在底层如何运行的知识。