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:
runMethod
in the final
case:runMethod
in the non-final
case: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.