Java中的内存栅是用来做什么的?

26
在尝试理解Java SE 9中新增的SubmissionPublisherOpenJDK 10源代码, Javadoc)类的实现时,我发现了一些之前不知道的VarHandle API调用: fullFenceacquireFencereleaseFenceloadLoadFencestoreStoreFence
经过一些研究,特别是关于内存屏障/栅栏的概念(我之前听说过它们,但从未使用过,因此对它们的语义非常陌生),我认为我对它们的基本用途有了一定的了解。尽管如此,由于我的问题可能来自误解,我想确保我首先就理解正确了。
记忆屏障是有关读写操作的重新排序限制。
根据它们是否对读或写设置约束,可以将内存屏障分为两大类:单向和双向内存屏障。 C ++支持各种内存屏障, 但这些与VarHandle提供的内存屏障不匹配。然而,在VarHandle中可用的一些内存屏障提供了与其相应的C ++内存屏障兼容的排序效果
  • #fullFenceatomic_thread_fence(memory_order_seq_cst)兼容
  • #acquireFenceatomic_thread_fence(memory_order_acquire)兼容
  • #releaseFenceatomic_thread_fence(memory_order_release)兼容
  • #loadLoadFence#storeStoreFence没有相应的C++部分
由于语义在细节上明显不同,因此“兼容”一词似乎非常重要。例如,所有C ++屏障都是双向的,而Java的屏障则不一定是(必须)。
  • 大多数内存屏障也具有同步效应。 这些效应特别取决于所使用的屏障类型和其他线程中先前执行的屏障指令。由于屏障指令的完整含义是与硬件相关的,因此我将坚持使用更高级别的(C ++)屏障。例如,在 C++ 中,在release屏障指令之前进行的更改对执行acquire屏障指令的线程可见。
  • 我的假设是否正确?如果是,我的问题如下:

    1. VarHandle中可用的内存屏障是否会导致任何类型的内存同步?

    2. 无论它们是否导致内存同步,Java 中重新排序约束可能有哪些用途?当涉及到 volatile 字段、锁或类似 #compareAndSetVarHandle 操作时,Java 内存模型已经提供了一些非常强的排序保证。

    BufferedSubscription为例,这是SubmissionPublisher的一个内部类(上面链接的源代码),在growAndAdd函数中建立了一个完整的屏障在第1079行。然而,我不确定这是用来做什么的。


    2
    我已经尝试回答,但简单来说,它们存在是因为人们想要比Java更弱的模式。按升序排列,它们分别是:plain -> opaque -> release/acquire -> volatile (sequential consistency) - Eugene
    1个回答

    18

    这主要是一个非答案(最初想把它作为评论,但你可以看到,它太长了)。只是我自己也对此进行了很多质疑,进行了大量的阅读和研究,现在可以毫不夸张地说:这很复杂。我甚至使用jcstress编写了多个测试以确定它们真正的工作方式(同时查看生成的汇编代码),虽然其中一些“某种程度上”讲得通,但总体上这个主题绝不容易。

    你需要理解的第一件事:

    Java语言规范(JLS)没有在任何地方提到屏障。对于Java来说,这将是一个实现细节:它真正以happens before语义为基础。为了能够根据JMM(Java内存模型)正确指定它们,JMM必须做出相当大的改变

    这是正在进行中的工作。

    其次,如果你真的想深入了解这里的知识点,这是要观看的第一件事。演讲非常精彩。我最喜欢的部分是Herb Sutter举起他的5个手指说:“这是能真正正确地处理这些问题的人数。”这应该给你一个复杂性的提示。然而,也有一些简单易懂的示例(例如多个线程更新的计数器,并不关心其他内存保证,只关心它本身是否被正确递增)。

    另一个例子是当你(在Java中)想要使用volatile标志来控制线程停止/启动时。

    volatile boolean stop = false; // on thread writes, one thread reads this    
    

    如果你使用java,你就会知道,没有volatile,这段代码是有问题的(例如,您可以阅读为什么双重锁定在没有它的情况下是错误的)。但是,您是否也知道,对于一些编写高性能代码的人来说,这太多了?volatile读/写还保证了顺序一致性 - 这具有一些强有力的保证,有些人想要一个更弱的版本。

    一个线程安全的标志,但不使用volatile?没错:VarHandle::set/getOpaque

    您可能会问,例如,为什么有人需要这个功能?并非每个人都对由volatile搭载的所有更改感兴趣。

    让我们看看如何在Java中实现这一点。首先,这样的奇特事物已经存在于API中:AtomicInteger::lazySet。这在Java内存模型中未指定,并且没有明确的定义;但仍然有人使用它(LMAX,afaik或更多阅读材料)。以我之见,AtomicInteger::lazySet就是VarHandle::releaseFence(或VarHandle::storeStoreFence)。


    让我们尝试回答为什么有人需要这些? Java内存模型基本上有两种访问字段的方法:plainvolatile(它保证了顺序一致性)。您提到的所有这些方法都是为了在这两者之间引入release/acquire语义;可能有一些情况,人们实际上需要这个。

    甚至可以更加放松一些,使用opaque,我仍在努力全面理解它。


    因此,最重要的是(顺便说一句,您的理解是相当正确的):如果您计划在Java中使用此功能-目前它们没有规范,请自行承担风险。如果您确实想了解它们,那么它们的C ++等效模式是开始的地方。


    3
    不要试图通过链接到古老的答案来弄清楚lazySet的含义,当前文档准确地说明了它的含义。此外,说JMM只有两种访问模式是误导性的。我们有volatile readvolatile write,它们可以一起建立一个happens-before关系。 - Holger
    1
    我正在撰写更多关于它的内容。考虑到cas既是读又是写,就像完整障碍一样,您可以理解为什么希望放松它。例如,在实现锁时,第一个动作是对锁计数器进行cas(0, 1),但您仅需要获取语义(如易失性读取),而解锁时将0写回应具有释放语义(如易失性写入),因此在解锁和后续锁定之间存在happens-before关系。相对于使用不同锁的线程,获取/释放甚至比易失性读取/写入更弱。 - Holger
    2
    @Peter Cordes: 第一个带有volatile关键字的C版本是C99,比Java晚了五年,但它仍然缺乏有用的语义,即使C++03也没有内存模型。C++称之为“原子”的东西也比Java年轻得多。而且,volatile关键字甚至不暗示原子更新。所以为什么要这样命名呢? - Holger
    1
    @PeterCordes 或许我把它和restrict混淆了,但是我记得有时候我不得不写__volatile来使用非关键字编译器扩展。所以也许它没有完全实现C89?别告诉我我那么老了。在Java 5之前,volatile更接近于C。但是Java没有MMIO,因此它的目的始终是多线程,但是Java 5之前的语义对此并不是很有用。因此添加了类似于释放/获取的语义,但仍然不是原子性的(原子更新是其上面构建的附加功能)。 - Holger
    2
    @Eugene 关于这个,我的例子是针对使用cas进行锁定的,这将是获取。倒计时门闩将承担原子递减与释放语义,随后线程达到零插入获取栅栏并执行最终操作。当然,在需要完整栅栏的原子更新的其他情况下,仍然需要完整栅栏。 - Holger
    显示剩余19条评论

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接