【问题标题】:How to understand JDK9 memory model?如何理解JDK9内存模型?
【发布时间】:2021-04-29 23:19:44
【问题描述】:

我正在学习 JDK9 内存模型。

看完演讲 Java Memory Model Unlearning Experience 并阅读论文 Using JDK 9 Memory Order Modes.

我对一些概念感到困惑。

  1. 不透明是否立即保证可见性

  2. 如何理解论文中的partial ordertotal order

对于第一个问题,论文说

使用裸旋转等待变量值几乎从来都不是一个好主意。使用 Thread.onSpinWait、Thread.yield 和/或阻塞同步来更好地应对“最终”可能需要很长时间这一事实,尤其是当系统上的线程数多于内核数时。

所以如果我写代码:

// shared variable I and VarHandle I_HANDLE which referred to I
public static int I = 0;

public static final VarHandle I_HANDLE;

// Thread-1
I_HANDLE.setOpaque(1);

// Thread-2
while((int) I_HANDLE.getOpaque() == 0){
}

线程 2 最终会终止,但可能会在很长一段时间后终止?

如果是这样,是否有任何最小的方法来保证线程 2 立即看到线程 1 的修改? (发布/获取?易失性?)

【问题讨论】:

    标签: java multithreading volatile java-9 java-memory-model


    【解决方案1】:

    没有像“立即”这样的更新。甚至电也以有限的速度运动。一般来说,在特定时间跨度内要求可感知的效果就像要求操作的特定执行时间一样。两者都无法保证,因为它们是 JVM 无法更改的底层架构的属性。

    当然,实际上,JVM 开发人员试图使操作尽可能快,而对于程序员而言,重要的是,对于线程间更新的可见性,没有比不透明写入更快的替代方案。更强的访问模式不会改变更新变得可见的速度,它们会为读取和写入的重新排序添加额外的约束。

    因此,在您的示例中,更新将在架构和系统负载允许的情况下尽快显示出来1,但不要询问实际数字。没有人能说这需要多长时间。如果您需要时间量方面的保证,您需要一个特殊的(“实时”)实现,它可以为您提供超出 Java 内存模型的额外保证。


    1 举一个实际场景:线程 1 和 2 可能竞争同一个 CPU。线程 1 写入值并在任务切换之前继续运行操作系统特定时间(甚至不能保证线程 2 是下一个)。这意味着写入后可能会经过相当长的时间,无论是挂钟时间还是线程 1 的进度。当然,其他线程也可能同时在其他 CPU 内核上取得很大进展。但也有可能线程 2 在线程 1 提交写入之前轮询是线程 1 没有机会写入新值的原因。这就是为什么您应该使用onSpinWaityield 标记此类轮询循环,以使执行环境有机会防止此类情况发生。请参阅this Q&A 讨论两者之间的区别。

    【讨论】:

    • 我不认为 onSpinWait 的目的与公平/饥饿有关。旋转的问题是管道可能会充满相同变量的推测性执行负载,并且这些负载可能会乱序执行。如果稍后的加载看到较旧的值而较早的加载看到较晚的值,这将成为问题。发生这种情况时,由于内存顺序冲突,会发生管道刷新。这是因为 x86 保证负载不会作为 TSO 的一部分重新排序。除了在循环退出时要付出的代价之外,它还可以防止 CPU 像疯了一样旋转。
    • @pveentjer 我从来没有说过任何关于公平的事情。而“疯狂旋转”恰恰是可以阻止写入任务获得 CPU 的原因。只要程序员了解此提示允许 JVM 消除轮询循环的特定于体系结构的缺点,细节就不那么重要了。它不必对每个架构都产生相同的影响。脚注只是一个示例场景。
    • @pveentjer 您正在从特定架构的特定实现中得出假设。规范没有说明 JVM 会做什么,这完全取决于实现者。该方法只是关于代码在做什么的提示。 JVM 实现者可以做任何他们认为对那个场景有用的事情。
    • @pveentjer 未记录的主要原因无关紧要。该规范允许实现者为所欲为。这个问答不是关于当前的 x86 实现,而是关于 JMM。这个方法是一个提示,仅此而已,点。无论如何,我为那些想了解实现细节的读者添加了一个链接。
    • 在这个级别上,它们非常重要。如果你玩这个级别,你需要知道你正在谈论的硬件,否则你不应该接触它。甚至最近将 Skylake 中 PAUSE 指令的延迟计数从 10 微秒增加到 140 微秒的变化也很重要。
    【解决方案2】:

    简单来说,不透明意味着读取或写入将要发生。所以它没有被编译器优化掉。

    它不提供关于其他变量的任何排序保证。

    因此,它适用于例如性能计数器,其中 1 个线程进行更新,其他线程读取它。

    但如果你会做以下(伪)

    // global
    final IntReference a = new IntReference();
    final IntReference b = new IntReference();
    
    void thread1(){
        a.setPlain(1);
        b.setOpaque(1);
    }
    
    void thread2(){
        int r1 = b.getOpaque();
        int r2 = a.getPlain();
        if(r1 == 1 && r2 == 0) println("violation");
    }
    

    那么可能是“违规”被打印出来,因为:

    • a、b 的商店重新排序
    • 来自 a 和 b 的负载被重新排序。

    但是,如果您要使用存储发布和加载获取,则不会发生重新排序,因为发布和获取提供了相对于其他变量的排序约束。

    void thread1(){
        a.setPlain(1);
        [StoreStore] <--
        [LoadStore]
        b.setRelease(1);
    }
    
    void thread2(){
        int r1 = b.getAcquire();
        [LoadLoad] <---
        [LoadStore]
        int r2 = a.getPlain();
        if(r1 == 1 && r2 == 0) println("violation");
    }
    

    【讨论】:

    • 我非常喜欢这个答案。我真正希望你也能更多地谈论所以它对于例如性能计数器(被称为“进度指标”,afaik)很有用。很高兴您也将发布/获取带入讨论。不过,可以使用volatile 完成与您在此处显示的相同的效果。我知道release/acquire 在某些平台上更便宜,这是一个很好的例子,但恕我直言(强调谦虚),它让读者困惑为什么volatile 没有在那里使用。无论如何,再一次,我真的很喜欢它。谢谢。
    猜你喜欢
    • 2019-10-08
    • 1970-01-01
    • 2011-06-03
    • 2018-03-31
    • 2020-04-22
    • 2020-12-13
    • 2019-03-24
    • 2020-02-21
    • 2011-02-25
    相关资源
    最近更新 更多