如果不需要该值,Java 是否允许优化掉易失性读取,同时删除同步之前发生的操作?

sto*_*r96 16 java multithreading volatile java-memory-model happens-before

以下代码示例显示了一种常见方法,用于演示由于缺少发生之前关系而导致的并发问题。

private static /*volatile*/ boolean running = true;
    
public static void main(String[] args) throws InterruptedException {
    new Thread() {
        @Override
        public void run() {
            while (running) {
                // Do nothing
            }
        }
    }.start();
    Thread.sleep(1000);
    running = false;
}
Run Code Online (Sandbox Code Playgroud)

如果runningvolatile,程序保证在大约一秒后终止。但是,如果running不是volatile,则根本无法保证程序终止(因为在running这种情况下没有发生之前关系或保证变量更改的可见性),而这正是我的测试中发生的情况。

根据JLS 17.4.5,人们还可以通过写入和读取另一个volatile变量来强制执行发生前关系running2,如以下代码示例所示。

private static boolean running = true;
private static volatile boolean running2 = true;
    
public static void main(String[] args) throws InterruptedException {
    new Thread() {
        @Override
        public void run() {
            while (running2 || running) {
                // Do nothing
            }
        }
    }.start();
    Thread.sleep(1000);
    running = false;
    running2 = false;
}
Run Code Online (Sandbox Code Playgroud)

volatile变量在每次循环迭代中被读取,并且当大约一秒后被running2读取时,由于发生前关系,还保证该变量被随后读取。因此,程序保证在大约一秒后终止,这正是我的测试中发生的情况。falserunningfalse

但是,当我将变量的读取放入循环内的running2空语句中时(如以下代码示例所示),程序不会在我的测试中终止。ifwhile

private static boolean running = true;
private static volatile boolean running2 = true;
    
public static void main(String[] args) throws InterruptedException {
    new Thread() {
        @Override
        public void run() {
            while (running) {
                if (running2) {
                    // Do nothing
                }
            }
        }
    }.start();
    Thread.sleep(1000);
    running = false;
    running2 = false;
}
Run Code Online (Sandbox Code Playgroud)

这里的想法是,volatile读取running2就像编译器内存屏障:编译器必须使汇编重新读取非volatile变量,因为读取running2可能与另一个线程中的释放操作同步。这将保证非易失性变量(如running.

但我的 JVM 似乎没有这样做。 这是编译器或 JVM 错误,还是 JLS 是否允许volatile在不需要值时删除读取的优化?(它仅控制一个空if主体,因此程序行为不依赖于正在读取的值,仅依赖于创建发生之前的关系。)

我认为 JLS 适用于源代码,并且由于running2is volatile,因此不应允许由于优化而删除读取变量的效果。这是编译器或 JVM 错误,还是存在实际上允许此类优化的规范?

小智 1

\n

这是 JVM 错误,还是 JLS 允许它在不需要值时删除易失性读取?

\n
\n

两者都不是。

\n

根据 JLS,此执行是有效的。

\n

第二个线程必须在读取后不久完成running2 == true
\n但是 JLS 不保证一个线程中的写入在另一个线程中变得可见所需的时间。
\n因此,您的程序执行是有效的,因为它对应于写入running2 = false需要很长时间才能传播到另一个线程的情况。

\n

顺便说一句,在我的 java 版本(OpenJDK 64 位服务器 VM(内部版本 17.0.3+7-suse-1.4-x8664,混合模式))上,程序在大约 1 秒内完成。\n这也是有效的执行 \xe2\x80\x94 这对应于写入更快地传播到第二个线程的
情况。running2 = false

\n

PS你提到“记忆障碍”。
\n对于内存屏障,通常存在一些最大时间,之后保证传播到其他线程。
\n但是 JLS 不按照内存屏障进行操作,也不必使用它们,并且实际上仅保证这一点

\n
\n

实现可以自由地生成它喜欢的任何代码,只要程序的所有结果执行都会生成可以由内存模型预测的结果。

\n
\n

PSS 如果您想查看 JVM 为您的程序生成的真实汇编代码,您可以使用+PrintAssembly

\n