Skip to content

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

@seaswalker

Description

@seaswalker

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

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也能正常退出。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions