【问题标题】:Unsafe publication in JavaJava 中的不安全发布
【发布时间】:2021-03-21 10:21:12
【问题描述】:

在 Brian Goetz 等人的《Java Concurrency in Practice》一书中:

如果您不能确保发布共享引用发生在另一个线程加载该共享引用之前,那么可以重新排序对新对象的引用的写入(从使用对象的线程的角度来看)写入其字段。在这种情况下,另一个线程可能会看到对象引用的最新值,但该对象的部分或全部状态(部分构造的对象)的值已过时。

这是否意味着:在线程发布对象中,对新对象的引用的写入不会随着对其字段的写入而重新排序;对其字段的写入发生在引用的写入之前。但是,该发布线程可能会在刷新更新的对象字段之前刷新对主内存的更新引用。因此,消费对象的线程可能会看到对象的非空引用,但会看到对象字段的过时值?从这个意义上说,为消费线程重新排序了操作。

【问题讨论】:

  • 请理解缓存总是一致的。所以在 1 cpu 上的 cacheline 上进行更改之前,它将首先在所有其他缓存上失效。因此,在写入高速缓存行之后,一个或多个 CPU 无法看到这种变化是不可能的。现代 CPU 不使用刷新和从主存储器读取。还要了解在常规加载/存储和易失性之间写入缓存没有区别。重新排序的原因是编译器或处理器,因为根据处理器的类型,存储/加载可能会乱序执行。

标签: java concurrency happens-before


【解决方案1】:

是的。

您的问题的答案就在您引用的段落中,并且您似乎在您的问题中呼应了答案。


但有一条评论:您说过,“[the] 发布线程可能会在刷新更新的对象字段之前刷新对主内存的更新引用。”如果您谈论的是 Java 代码,那么最好坚持使用 Java 语言规范 (JLS) 中编写的内容。

JLS 告诉您 Java 程序的行为方式。它没有提到“主内存”或“缓存”或“刷新”。它只是说,如果没有显式同步,从某个其他线程的角度来看,一个线程以特定顺序对两个或多个变量执行的更新可能似乎以不同的顺序发生。如何或为什么会发生这种情况是“实施细节”。

【讨论】:

  • 感谢您的回答。是的,我改写了我引用的段落中的内容,因为当它只是说“从消费线程的角度重新排序”时,如果不猜测一下是什么意思,这并不是很明显。我理解你所说的实现细节,我只是给出了一个可能的例子来说明实现可能是什么。
【解决方案2】:
  1. 在发布对象的线程中,对新对象的引用的写入不会随着对其字段的写入而重新排序;对其字段的写入发生在引用的写入之前。

是的。因为在一个线程中,这个过程发生在不允许重新排序的程序顺序中:“如果 x 和 y 是同一线程的操作,并且 x 在程序顺序中位于 y 之前,则 hb(x, y)。” (https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.5)。我们可以改写一下:“对新对象的引用的写入不会随着对其字段的写入而重新排序”,这意味着如果您读取对对象的引用,则可以保证您将连续读取其所有字段.

  1. 使用对象的线程可能会看到对象的非空引用,但会看到对象字段的过时值?

是的,当您以不安全的方式发布对象时,可能会出现这种情况,而没有使用内存屏障实现适当的 HB 边缘。从字面上看,在没有 HB/membars 的情况下,您会得到未定义的行为。这意味着在其他线程中,您可以查看/读取任何内容(JMM 明确禁止的空气中的 (OoTA) 值除外)。安全发布使所有观察发布对象的读者都可以看到发布之前写入的所有值。确保出版物安全的最流行和最简单的方法很少:

您还可以使用其他产生 HB 的操作,如 Thread.start() 等,但我的日常最爱是:

  • 不可变数据的最终字段
  • volatile/AtomicXXX 字段和锁(显式同步块/ReadWriteLock,BlockingQueue 中的隐式锁)用于可变数据。

【讨论】:

  • 小正确:在 C++ 上,数据竞争会产生未定义的行为,但在 JVM 上并非如此。
  • @pveentjer “(UB) 是执行程序的结果,该程序的行为被规定为不可预测的”。 IMO,关键词是“不可预测”。就允许 OOTA 值的 C/C++ 而言,我们无法预测可以读取什么值以及何时可以读取任何值。 JMM 不允许 OOTA 值,我们知道可能读取的值,但我们仍然无法预测读取器(线程)何时会看到特定值。假设我们将一个没有任何 HB 的布尔值从默认的 false 修改为 true。我们可以读取的唯一值是 { false, true},但我们无法预测读者何时看到 true...
  • 在数据竞争的情况下,Java 中可能失败的内容比 C++ 中的限制要多得多。只有 array.length、强制转换、虚拟方法以外的线程间操作可能会失败。 docs.oracle.com/javase/specs/jls/se7/html/…。在 C++ 中,一切都可能失败。例如,在 C++ 中,您可以对损坏的指针进行方法调用,然后在内存中的任何位置结束,一切都会发生。 AFAIK 术语“未定义的行为”是指“任何事情发生”的情况。
  • @pveentjer 我们所说的“失败”是什么意思? :) 例如,当我的程序永远不会取得我期望它必须取得的进展时,这是“失败”吗?没错,您使用的“未定义行为”似乎是特定于 C 的。我只是说没有 HB 我无法“预测”我的 Java 程序何时/如何取得任何进展,我不知道我的程序将如何进行
  • 当然。这是一条非常细的线。我同意,如果你在 Java 中有数据竞赛,你也可能会遇到问题。用枪或火箭射击,效果相似。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-11-14
  • 1970-01-01
  • 2017-10-08
  • 1970-01-01
  • 2020-08-30
  • 1970-01-01
相关资源
最近更新 更多