Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

令人迷惑的volatile例子(二) #11

Open
seaswalker opened this issue Jul 4, 2020 · 0 comments
Open

令人迷惑的volatile例子(二) #11

seaswalker opened this issue Jul 4, 2020 · 0 comments

Comments

@seaswalker
Copy link
Owner

seaswalker commented Jul 4, 2020

读了一次“错误”的并发控制引发的思考一文,觉得有些疑问。对于下面的代码:

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关闭锁消除优化就行了。
我的疑问在于:

  1. 内存屏障需要成对使用,对flag的写入并没有同步措施,以保证多个变量的内存操作顺序
  2. 上面代码实际上对单个变量的读写操作,我认为这种情况在硬件(尤其是x86)层面来说不需要任何内存屏障,缓存一致性协议即可保证变量对其它CPU核心的全局可见,这一点参考 内存屏障(对硬件) #10Does a memory barrier ensure that the cache coherence has been completed?,高赞回答的核心逻辑就是: 对于单个变量,缓存一致性协议即可保证对CPU全局可见,内存屏障只是促使(加速)了这一点,所以在这种情况下加不加内存屏障只是一个快和慢的问题,不是可见与不可见的问题
  3. 我觉得上面的代码其实还是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激进优化的问题
两次汇编代码的优化级别都是:
image
其实,我的例子在不加volatile的情况下使用JVM参数-XX:-UseOnStackReplacement也能正常退出。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant