Thread.yield 是否保证会刷新 reads/writes 到内存?

Is Thread.yield guaranteed to flush reads/writes to memory?

让我们保存一下,我有这段代码,它显示线程读取过时的缓存,这会阻止它退出 while 循环。

class MyRunnable implements Runnable {
    boolean keepGoing = true; // volatile fixes visibility
    @Override public void run() {
        while ( keepGoing ) {
            // synchronized (this) { } // fixes visibility
            // Thread.yield(); // fixes visibility
            System.out.println(); // fixes visibility 
        }
    }
}
class Example {
    public static void main(String[] args) throws InterruptedException{
        MyRunnable myRunnable = new MyRunnable();
        new Thread(myRunnable).start();
        Thread.sleep(100);  
        myRunnable.keepGoing = false;
    }
}

我相信 Java 内存模型可以保证所有对 volatile 变量的写入与来自任何线程的所有后续读取同步,从而解决问题。

如果我的理解是正确的,同步块生成的代码也会清除所有挂起的读取和写入,作为一种“内存屏障”并解决问题。

从实践中我已经看到插入 yieldprintln 也会使变量更改对线程可见并且它正确退出。我的问题是:

yield/println/io 是 JMM 以某种方式保证的内存屏障,还是不能保证有效的幸运副作用?


编辑: 至少我在这个问题的措辞中所做的一些假设是错误的,例如关于同步块的假设。我鼓励问题的读者阅读下面发布的答案中的更正。

没有。 ,JLS 或 类 的 javadoc 或您在那里使用的方法

都没有保证。

当前实现中,在yield()println中有在实践中memory barriers . (如果你深入挖掘实现代码,你应该能够弄清楚它们是如何产生的以及它们的作用是什么。)

但是,不能保证所有平台上 Java1 的所有实现都存在这些内存障碍。规范 未指定 发生在 关系存在 2 之前,因此它们不需要 3个要插入的内存屏障。

假设:

  • 假设Thread.yield() was implemented as a no-op。 (与 System.gc() 可以是空操作的方式相同。)

  • 假设输出流堆栈以一种不再需要在引擎盖下同步的方式进行了优化。例如,假设 JVM 可以推断出特定的输出流是线程受限的,并且在写入其缓冲区时不需要内存屏障。

现在我个人认为这些变化不太可能发生。 (而且它们甚至可能不可行。)但如果它们真的发生了,目前依赖于那些偶然的内存障碍的相当多的“损坏”应用程序很可能会停止工作。

重点是:如果您想要保证,请以规格为准。规范是唯一真正的保证……如果您的代码需要可移植。


1 - 特别是未来的。
2 - 正如 Holger 的回答所解释的那样,Thread 的 javadocs 明确指出你 不能 假设或依赖 yield() 发生的任何同步行为。这显然意味着在 yield() 和任何其他线程上的任何操作之间 之前没有 发生。
3 - 内存屏障实际上是​​一个实现细节。 typical 编译器使用它们来实现 JMM 的可见性保证。关键是保证,而不是用于实施它们的策略。因此,当您尝试计算多线程代码是否正确时,任何关于内存屏障、高速缓存、寄存器等的讨论都离题

Lets save I have this code which exhibits stale cache reads by a thread, which prevent it from exiting its while loop.

如果您指的是 CPU 缓存,那么这是一个糟糕的心智模型(除了不适合 JMM 的心智模型之外)。现代 CPU 上的缓存始终是连贯的。

I believe the Java Memory Model guarantees that all writes to a volatile variable synchronize with all subsequent reads from any thread, so that solves the problem.

没错。在写入 volatile 变量和随后读取同一 volatile 变量之间有一个 happens before edge。

Blockquote If my understanding is correct the code generated by a synchronized block also flushes out all pending reads and writes, which serves as a kind of "memory barrier" and fixes the issue.

与 JMM 结合使用内存屏障进行推理是危险的。

https://shipilev.net/blog/2016/close-encounters-of-jmm-kind/#myth-barriers-are-sane

在监视器的释放和同一监视器的任何后续获取之间存在先行边缘。因此,如果您在受锁保护的情况下访问 keepGoing 变量,则不会发生数据竞争。

Is yield/println/io serving as a memory barrier guarenteed in some way by the JMM or is it a lucky side effect that cannot be guaranteed to work?

检查JLS,您会看到在 2 个产量之间的边缘之前没有发生任何事情。可能涉及 CPU 内存屏障,但问题可能会在代码到达 CPU 之前发生。例如。 JIT 可能会将代码优化为:

if(!keepGoing){
   return;
}

while(true){
   Thread.yield();
   println();
}

所以在这种情况下,代码在 CPU 上执行之前已经是 'broken',因为代码永远不会看到 'keepGoing' 变量的更新版本。

我不确定 Thread.yield() 是否有任何编译器障碍,如果存在编译器障碍则 JIT 无法优化加载或存储。但是 none 这是规范的一部分。

规范中没有任何内容保证任何类型的冲洗。这完全是错误的心智模型,假设必须有一些东西,比如维护全局状态的主内存。但是执行环境可以在每个 CPU 处都有本地内存,而根本没有主内存。因此 CPU1 将更新的数据发送到 CPU2 并不意味着 CPU3 知道它。

实际上,系统有一个主内存,但缓存可能会在不需要将数据传输到主内存的情况下同步。

此外,讨论内存传输最终会陷入狭隘的视野。 Java 的内存模型还规定了 JVM 可以执行哪些优化,哪些不可以。例如

nonVolatileVar = null;
Thread.sleep(100_000);
if(nonVolatileVar == null) {
  // do something
}

这里,编译器有权去掉条件,无条件执行block,因为前面的语句(忽略sleep)写了null,其他线程的活动与非[=12无关=] 变量,不管经过了多少时间。

因此,当执行此优化后,有多少线程将新值写入此变量并“刷新到内存”并不重要。此代码不会引起注意。

所以我们来咨询一下the specification

It is important to note that neither Thread.sleep nor Thread.yield have any synchronization semantics. In particular, the compiler does not have to flush writes cached in registers out to shared memory before a call to Thread.sleep or Thread.yield, nor does the compiler have to reload values cached in registers after a call to Thread.sleep or Thread.yield.

我想,你的问题的答案再明确不过了。

完整性

I believe the Java Memory Model guarantees that all writes to a volatile variable synchronize with all subsequent reads from any thread, so that solves the problem.

在写入 volatile 变量之前进行的所有写入将对随后读取 相同变量 的线程可见。因此,在您的情况下,将 keepGoing 声明为 volatile 将解决此问题,因为两个线程始终使用它。

If my understanding is correct the code generated by a synchronized block also flushes out all pending reads and writes, which serves as a kind of "memory barrier" and fixes the issue.

离开 synchronized 块的线程与使用 进入 synchronized 块的线程建立 happens-before 关系对象。如果在一个线程中使用 synchronized 块似乎可以解决问题,尽管您没有在另一个线程中使用 synchronized 块,则您依赖于特定实现的副作用,但不能保证继续工作。