Java中的内存防护有什么用?


18

在尝试了解如何实现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中可能有什么用?Java内存模型已经就可变字段,锁或VarHandle类似操作的顺序提供了一些非常有力的保证#compareAndSet

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


1
我试图回答,但说​​得很简单,之所以存在,是因为人们想要一种比Java更弱的模式。按升序排列的是:plain -> opaque -> release/acquire -> volatile (sequential consistency)
尤金

Answers:


11

实际上,这主要是一个无答案的内容(最初想将其设为评论,但您会看到,它太长了)。只是我自己问了很多问题,做了很多阅读和研究,因此我可以放心地说:这很复杂。我什至用jcstress编写了多个测试,以弄清它们是如何工作的(在查看生成的汇编代码的同时),以及其中某些以某种方式有意义,但总的来说,这个主题绝非易事。

您需要了解的第一件事:

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

这项工作正在进行中。

其次,如果您真的想在这里刮擦表面,这是首先要注意的事情。谈话真是不可思议。我最喜欢的部分是Herb Sutter举起5根手指说:“这是多少人可以真正,正确地使用它们。” 那应该给您提示所涉及的复杂性。但是,有一些简单的示例很容易掌握(例如,由多个线程更新的计数器并不关心其他实例)内存保证,而只关心它本身是否正确递增)。

另一个示例是(在Java中)您希望volatile标志控制线程停止/启动的时间。您知道,经典的:

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

如果使用Java,您会知道没有 volatile破坏此代码(例如,您可以了解为什么没有它就会破坏双重检查锁定的原因)。但是您是否也知道,对于某些编写高性能代码的人来说,这太多了吗?volatile读/写还可以保证顺序的一致性 -有一些有力的保证,有些人则希望对此有较弱的版本。

线程安全标志,但不是易失的?是的,完全是:VarHandle::set/getOpaque

您会质疑为什么有人可能需要它?并非每个人都对由操作系统支持的所有更改感兴趣volatile

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


让我们尝试回答为什么有人需要这些吗?

JMM基本上具有两种方式来访问一个字段:平原挥发性(这保证顺序一致性)。您提到的所有这些方法都可以在这两个发布/获取语义之间带来一些好处;我想有些情况下,人们实际上需要这样做。

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


因此,底线(您的理解是相当正确的,顺便说一句):如果您打算在Java中使用它-目前他们尚无规范,则后果自负。如果您确实想了解它们,则可以从它们的C ++等效模式开始。


1
不要试图lazySet通过链接到古老的答案来弄清含义,当前文档准确地说明了它的含义。此外,说JMM只有两种访问模式是一种误导。我们有挥发性读取挥发性写,它们一起可以建立之前发生的关系。
Holger

1
我正在写更多有关它的文章。考虑到cas既是读取又是写入,就像一个完整的障碍,并且您可能会理解为什么需要放宽它。例如,当实施锁时,第一个操作是对锁计数的cas(0,1),但是您只需要获取语义(如volatile读取),而最终写入0来解锁应该具有释放语义(如volatile写入) ),因此在解锁和随后的锁定之前会发生一次。对于使用不同锁的线程,“获取/释放”甚至比“易失性读/写”弱。
Holger

1
@Peter Cordes:第一个带有volatile关键字的C版本是Java 五年后的 C99,但是它仍然缺乏有用的语义,即使C ++ 03也没有内存模型。C ++称为“原子”的事物也比Java年轻得多。而且该volatile关键字甚至不暗示原子更新。那么为什么要这样命名呢?
Holger

1
@PeterCordes也许是,我将其与混淆restrict,但是,我记得曾经不得不编写__volatile以使用非关键字编译器扩展的情况。那么也许,它没有完全实现C89?不要告诉我我那么大。在Java 5之前,volatile它更接近于C。但是Java没有MMIO,因此它的目的始终是多线程,但是Java 5之前的语义在此方面不是很有用。因此,添加了类似语义的发布/获取,但仍然不是原子的(原子更新是它之上构建的附加功能)。
霍尔格

2
@Eugene 对此,我的示例专门针对使用cas进行锁定而获得。倒计时锁存器将具有释放语义的原子递减,然后线程达到零,插入获取围墙并执行最终操作。当然,在其他情况下,原子更新仍然需要完整的防护。
Holger
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.