Open
Description
读了一次“错误”的并发控制引发的思考一文,觉得有些疑问。对于下面的代码:
class MultiProcessorTask {
private boolean flag = true;
public void runMethod() {
while (flag) {
synchronized (new Simple(1)){}
}
}
public void stopMethod() {
System.out.println("change 'flag' field ...");
flag = false;
}
}
原文的观点似乎倾向于synchronized
带来的happens-before规则可以保证对flag
的可见性,所以需要用JVM参数-XX:-EliminateLocks
关闭锁消除优化就行了。
我的疑问在于:
- 内存屏障需要成对使用,对flag的写入并没有同步措施,以保证多个变量的内存操作顺序
- 上面代码实际上对单个变量的读写操作,我认为这种情况在硬件(尤其是x86)层面来说不需要任何内存屏障,缓存一致性协议即可保证变量对其它CPU核心的全局可见,这一点参考 内存屏障(对硬件) #10 和Does a memory barrier ensure that the cache coherence has been completed?,高赞回答的核心逻辑就是: 对于单个变量,缓存一致性协议即可保证对CPU全局可见,内存屏障只是促使(加速)了这一点,所以在这种情况下加不加内存屏障只是一个快和慢的问题,不是可见与不可见的问题。
- 我觉得上面的代码其实还是JIT如何优化的问题,关闭锁消除优化->while循环内含有锁->锁阻止了JIT生成死循环代码(猜测)
所以我把代码改写成了没有锁和volatile
:
package test;
class MultiProcessorTask {
private boolean flag = true;
long sum = 0L;
public void runMethod() {
while (flag) {
long a = System.currentTimeMillis() % 9;
if (a == 1L) {
sum += a;
}
}
System.out.println("Result: " + sum);
}
public void stopMethod() throws InterruptedException {
System.out.println("准备睡眠1秒,然后置flag为false.");
Thread.sleep(1000);
System.out.println("change 'flag' field ...");
flag = false;
}
}
class ThreadA extends Thread {
private MultiProcessorTask task;
ThreadA(MultiProcessorTask task) {this.task = task;}
@Override
public void run() {
task.runMethod();
}
}
public class TestRun {
public static void main(String[] args) throws InterruptedException {
MultiProcessorTask task = new MultiProcessorTask();
ThreadA a = new ThreadA(task);
a.start();
task.stopMethod();
System.out.println("it's over");
}
}
直接运行,不会退出,加上JVM参数-Xint
解释执行,会退出,这一步就说明了这个锅还是JIT的,下面通过jitwatch看一下JIT优化后的汇编代码。使用的JVM参数是:
-server -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+PrintAssembly -XX:+LogCompilation -XX:LogFile=live.log
没有volatile
时:
L0001: movabs $0x108289ce4,%r10
0x00000001117cc1f6: callq *%r10 ;*invokestatic currentTimeMillis
; - test.MultiProcessorTask::runMethod@7 (line 10)
0x00000001117cc1f9: mov %rax,%r10
0x00000001117cc1fc: mov %rax,%r11
0x00000001117cc1ff: sar $0x3f,%r11
0x00000001117cc203: movabs $0x1c71c71c71c71c72,%rax
0x00000001117cc20d: mov %r10,%r8
0x00000001117cc210: imul %r10
0x00000001117cc213: sub %r11,%rdx ;*lrem
; - test.MultiProcessorTask::runMethod@13 (line 10)
0x00000001117cc216: mov %rdx,%r10
0x00000001117cc219: shl $0x3,%r10
0x00000001117cc21d: add %rdx,%r10
0x00000001117cc220: mov %r8,%r11
0x00000001117cc223: sub %r10,%r11
0x00000001117cc226: cmp $0x1,%r11
0x00000001117cc22a: jne L0002 ;*ifne ;如果余数不是1
; - test.MultiProcessorTask::runMethod@18 (line 11)
0x00000001117cc22c: incq 0x10(%rbp) ; OopMap{rbp=Oop off=144}
;*goto
; - test.MultiProcessorTask::runMethod@31 (line 14)
L0002: test %eax,-0xa279236(%rip) # 0x0000000107553000
;*goto
; - test.MultiProcessorTask::runMethod@31 (line 14)
; {poll} *** SAFEPOINT POLL ***
0x00000001117cc236: jmp L0001
L0003: xor %ebp,%ebp
0x00000001117cc23a: jmp L0000
可以看出,里面形成了一个死循环,不再判断flag
的值,甚至也不把为1的余数加到sum
中,每次循环只是取当前时间,然后取余。
而给flag
加上volatile
后的汇编代码为:
0x0000000119be7861: jmp L0002
L0000: mov 0x10(%rbx),%r10 ;*getfield sum; 余数是1时跳到这里,取sum加总
; - test.MultiProcessorTask::runMethod@23 (line 12)
0x0000000119be7867: add $0x1,%r10
0x0000000119be786b: mov %r10,0x10(%rbx) ;*putfield sum
; - test.MultiProcessorTask::runMethod@28 (line 12)
0x0000000119be786f: nop ; OopMap{rbx=Oop off=80}
;*goto
; - test.MultiProcessorTask::runMethod@31 (line 14)
L0001: test %eax,-0xc6fd876(%rip) # 0x000000010d4ea000; 余数不是1跳到这里,取flag测试继续循环
;*aload_0
; - test.MultiProcessorTask::runMethod@0 (line 9)
; {poll} *** SAFEPOINT POLL ***
L0002: movzbl 0xc(%rbx),%r11d ;*getfield flag
; - test.MultiProcessorTask::runMethod@1 (line 9)
0x0000000119be787b: test %r11d,%r11d; 测试flag是不是为false
0x0000000119be787e: je L0003 ;*ifeq; 是false,跳到L0003退出循环
; - test.MultiProcessorTask::runMethod@4 (line 9)
0x0000000119be7880: movabs $0x10e289ce4,%r10
0x0000000119be788a: callq *%r10 ;*invokestatic currentTimeMillis
; - test.MultiProcessorTask::runMethod@7 (line 10)
0x0000000119be788d: mov %rax,%r11
0x0000000119be7890: movabs $0x1c71c71c71c71c72,%rax
0x0000000119be789a: imul %r11
0x0000000119be789d: mov %r11,%r10
0x0000000119be78a0: sar $0x3f,%r10
0x0000000119be78a4: sub %r10,%rdx ;*lrem
; - test.MultiProcessorTask::runMethod@13 (line 10)
0x0000000119be78a7: mov %rdx,%r10
0x0000000119be78aa: shl $0x3,%r10
0x0000000119be78ae: add %rdx,%r10
0x0000000119be78b1: sub %r10,%r11
0x0000000119be78b4: cmp $0x1,%r11
0x0000000119be78b8: je L0000 ;*ifne; 如果余数是1,那么跳到L0000
; - test.MultiProcessorTask::runMethod@18 (line 11)
0x0000000119be78ba: jmp L0001; 余数不是1,跳到L0001
L0003: mov $0xffffff65,%esi
代码不同一目了然了。所以,在针对单个变量的前提下,不管是volatile
还是加锁各种花式操作,所针对的都不是硬件层面上的可见性问题,而是如何阻止JIT激进优化的问题。
两次汇编代码的优化级别都是:
其实,我的例子在不加volatile
的情况下使用JVM参数-XX:-UseOnStackReplacement
也能正常退出。
Metadata
Metadata
Assignees
Labels
No labels