在 Java 中,构造函数中最终的字段赋值涉及哪些操作?

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

如果 Simple class 字段 afinal 关键字。

如果a是一个final字段,这个程序会正常退出; 如果是普通字段,这个程序会一直保持运行。

这种情况只出现在C2编译器中。

我认为这种情况与flag字段在多线程中的可见性有关environment.However,我尝试通过观察汇编代码hsdis ,发现有无final关键字的区别。

我没发现什么不同。

实际上,我知道存储 "final" 字段不会在 x86 平台上发出任何汇编指令。可是为什么会出现这种情况呢?有没有一些我不知道的特殊操作?

感谢阅读。

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 final case

the non-final case

你反汇编错了。我的意思是,两个屏幕截图上都有一个独立编译的 runMethod,但是,它在现实中从未执行过。相反,执行从解释器跳转到 OSR 存根。您需要查找标有 % 符号(表示堆栈替换)的编译。

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

非最终案例和最终案例的编译代码存在差异。我只留下了相关部分:

非最终版

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

决赛

  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

的确,第一种情况编译成无限循环,而第二种情况保留字段检查。

你看,在这两种情况下根本没有 Simple 实例分配,也没有字段分配。因此,这不是用于编译 final 字段分配的指令的问题,而是编译器级别的障碍,它可以防止在循环之外缓存 flag 字段。

但是由于完全消除了分配,因此 final 字段分配所隐含的障碍也可以消失。在这里我们看到了一个错过的优化机会。事实上,这种错过的优化在较新的 JVM 版本中得到了修复。如果您 运行 在 JDK 11 上使用相同的示例,则无论 final 修饰符如何,这两种情况都会出现无限循环。