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

Qua*_*fel 21 java concurrency memory-model java-memory-model memory-barriers

在试图了解SubmissionPublisherJava SE 10 中的源代码,OpenJDK | docs),在版本 9 中添加到 Java SE 的新类是如何实现的,我偶然发现了一些VarHandle我以前不知道的API 调用:

fullFenceacquireFencereleaseFenceloadLoadFencestoreStoreFence

在做了一些研究之后,特别是关于内存屏障/栅栏的概念(我以前听说过它们,是的;但从未使用过它们,因此对它们的语义非常不熟悉),我想我对它们的用途有了基本的了解. 尽管如此,由于我的问题可能源于误解,我想确保我首先做对了:

  1. 内存屏障是关于读写操作的重新排序约束。

  2. 内存屏障可以分为两大类:单向和双向内存屏障,这取决于它们是否对读取或写入或两者都设置了约束。

  3. C++ 支持多种内存屏障,但是,这些与VarHandle. 然而,一些在可用内存壁垒VarHandle提供排序的影响兼容其相应的C ++内存屏障。

    • #fullFence 兼容 atomic_thread_fence(memory_order_seq_cst)
    • #acquireFence 兼容 atomic_thread_fence(memory_order_acquire)
    • #releaseFence 兼容 atomic_thread_fence(memory_order_release)
    • #loadLoadFence并且#storeStoreFence没有兼容的 C++ 计数器部分

兼容这个词在这里似乎非常重要,因为在细节方面语义明显不同。例如,所有 C++ 屏障都是双向的,而 Java 的屏障不是(必然)。

  1. 大多数内存屏障也具有同步效应。那些特别依赖于其他线程中使用的屏障类型和先前执行的屏障指令。由于屏障指令的全部含义是特定于硬件的,因此我将坚持使用更高级别的 (C++) 屏障。例如,在 C++ 中,在释放屏障指令之前所做的更改对于执行获取屏障指令的线程是可见的。

我的假设正确吗?如果是这样,我产生的问题是:

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

  2. 不管它们是否会导致内存同步,重新排序约束在 Java 中有什么用?当涉及易失性字段、锁或VarHandle操作时,Java 内存模型已经提供了一些非常强大的关于排序的保证#compareAndSet

如果您正在寻找一个例子:前面提到BufferedSubscriptionSubmissionPublisher(上面链接的源)的内部类,在第 1079 行建立了一个完整的围栏(功能growAndAdd;因为链接的网站不支持片段标识符,只需按 CTRL+F 即可)。但是,我不清楚它的用途是什么。

Eug*_*ene 15

这主要是一个非答案,真的(最初想发表评论,但正如你所看到的,它太长了)。只是我自己对此提出了很多质疑,做了很多阅读和研究,此时我可以有把握地说:这很复杂。我什至用jcstress编写了多个测试来弄清楚它们是如何工作的(在查看生成的汇编代码时),虽然其中一些在某种程度上是有道理的,但总的来说,这个主题绝非易事。

您首先需要了解的是:

Java 语言规范 (JLS) 在任何地方都没有提到屏障。对于 Java 来说,这将是一个实现细节:它实际上是在语义之前发生的。为了能够根据 JMM(Java 内存模型)正确指定这些,JMM 必须进行很多更改

这是正在进行中的工作。

其次,如果你真的想在这里触及表面,这是首先要看的。谈话令人难以置信。我最喜欢的部分是 Herb Sutter 举起他的 5 根手指说:“这是有多少人可以真正正确地使用这些。” 这应该会给你一个所涉及的复杂性的暗示。尽管如此,还是有一些简单的例子很容易掌握(比如一个由多个线程更新的计数器,它不关心其他内存保证,只关心它本身是否正确递增)。

另一个例子是(在 Java 中)你想要一个volatile标志来控制线程停止/启动。你知道的,经典的:

volatile boolean stop = false; // on thread writes, one thread reads this    
Run Code Online (Sandbox Code Playgroud)

如果您使用 java,您会知道没有 volatile此代码会被破坏(例如,您可以阅读为什么没有它会破坏双重检查锁定)。但是您是否也知道对于一些编写高性能代码的人来说这太过分了?volatile读/写也保证了顺序一致性——有一些强有力的保证,有些人想要一个较弱的版本。

线程安全标志,但不是 volatile?是的,正是:VarHandle::set/getOpaque

例如,您会质疑为什么有人可能需要它?并不是每个人都对由volatile.

让我们看看我们将如何在 Java 中实现这一点。首先,API 中已经存在这种奇特的东西:AtomicInteger::lazySet. 这在 Java 内存模型中没有指定,也没有明确的定义;仍然有人使用它(LMAX、afaik 或this 以供更多阅读)。恕我直言,AtomicInteger::lazySetVarHandle::releaseFence(或VarHandle::storeStoreFence)。


让我们试着回答为什么有人需要这些

JMM 基本上有两种访问字段的方法:plainvolatile(保证顺序一致性)。你提到的所有这些方法都是为了在这两者之间带来一些东西 -释放/获取语义;我想在某些情况下,人们确实需要这个。

一个甚至更多的松弛释放/获取将是不透明的,这我仍然试图充分了解


因此底线(你的理解是相当正确的,顺便说一句):如果你打算在java中使用它 - 他们目前没有规范,请自担风险。如果您确实想了解它们,可以从它们的 C++ 等效模式开始。

  • 不要试图通过链接到古老的答案来弄清楚“lazySet”的含义,[当前文档](https://docs.oracle.com/en/java/javase/13/docs/api/java.base /java/util/concurrent/atomic/AtomicInteger.html#lazySet(int)) 准确地说明了它的含义。此外,说 JMM 只有两种访问模式也是一种误导。我们有*易失性读取*和*易失性写入*,它们一起可以建立*发生在*之前的关系。 (3认同)
  • @Peter Cordes:第一个具有“易失性”关键字的 C 版本是 C99,在 Java 之后五年,但它仍然缺乏有用的语义,甚至 C++03 也没有内存模型。C++ 称之为“原子”的东西也比 Java 年轻得多。而且“易失性”关键字甚至并不意味着原子更新。那么为什么要这样命名呢。 (2认同)
  • @Eugene关于[这个](/sf/ask/4208341861/?noredirect=1#comment106634722_60130907),我的例子是特定于使用用于锁定的 cas 将被获取。倒计时锁存器将带有释放语义的原子递减,随后线程达到零插入获取栅栏并执行最终操作。当然,还有其他原子更新情况,仍然需要完整的栅栏。 (2认同)

归档时间:

查看次数:

770 次

最近记录:

5 年,10 月 前