Search code examples
javamultithreadingx86jvmjava-memory-model

In Java , what operations are involved in the final field assignment in the constructor?


I got different execution result from the follow code if the Simple class field a was modified by final keyword.

If the a is a final field , this program will normally exit; If it's a plain field, this program will keep running all the time.

This situation only occurs in C2 compiler .

I thought this situation is related to visibility of the flag field in multi-threads environment.However, I try to observed the assembly code by hsdis ,and found the difference between with and without final keyword.

I found nothing difference.

Actually, I know the storing "final" field would not emit any assembly instructions on x86 platform. But why this situation came out? Are there some particular operations I don't know ?

Thanks for reading.

class MultiProcessorTask {

    private boolean flag= true;

    public void runMethod() {
        while (flag) {
            new Simple(1);
        }
    }

    public void stopMethod() {
        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();
    }
}

class Simple {
    private int a;  // modify "a" as "final"

    Simple(int a) {this.a = a;}
}

public class TestRun {
    public static void main(String[] args) {
        MultiProcessorTask task = new MultiProcessorTask();
        ThreadA a = new ThreadA(task);
        a.start();
        task.stopMethod();
        System.out.println("it's over");
    }
}

The disassembly code output:

  • The runMethod in the final case:

the final case

  • The runMethod in the non-final case:

the non-final case


Solution

  • You've disassembled the wrong compilation. I mean, there is a standalone compiled runMethod on both screenshots, however, it is never executed in reality. Instead, execution jumps from the interpreter to the OSR stub. You need to look for a compilation marked with % sign (which denotes on-stack replacement).

    Compiled method (c2)     646  662 %           MultiProcessorTask::runMethod @ 0 (20 bytes)
                                      ^
                                     OSR
    

    Here is difference in the compiled code between non-final and final cases. I left only the relevant part:

    non-final

      0x000000000309ae31: test   %eax,-0x5aae37(%rip)   ; safepoint poll
      0x000000000309ae37: jmp    0x000000000309ae31     ; loop
    

    final

      0x0000000002c3a3a0: test   %eax,-0x265a3a6(%rip)  ; safepoint poll
      0x0000000002c3a3a6: movzbl 0xc(%rbx),%r11d        ; load 'flag' field
      0x0000000002c3a3ab: test   %r11d,%r11d
      0x0000000002c3a3ae: jne    0x0000000002c3a3a0     ; loop if flag == true
    

    Indeed, the first case is compiled to an inifite loop, while the second one retains the field check.

    You see, in both cases there is no Simple instance allocation at all, and no field assignment either. So, it's not a matter of instructions used to compile the final field assignment, but rather a compiler level barrier which prevents from caching the flag field out of the loop.

    But since the allocation is eliminated altogether, the barrier implied by final field assignment can go away, too. Here we see just a missed optimization opportunity. And in fact, this missed optimization was fixed in newer JVM versions. If you run the same example on JDK 11, there will be an infinite loop in both cases, regardless of the final modifier.