【问题标题】:Cheapest way of establishing happens-before with non-final field使用非最终字段建立之前发生的最便宜的方法
【发布时间】:2014-04-29 04:44:58
【问题描述】:

许多问题/答案表明,如果一个类对象有一个final 字段,并且在构造过程中没有对它的引用暴露给任何其他线程,那么一旦构造函数,所有线程都可以保证看到写入该字段的值完成。他们还指出,将对从未被外部线程访问过的可变对象的引用存储到final 字段中将确保在存储之前对该对象进行的所有突变在所有访问的线程上都是可见的对象通过字段。不幸的是,这两种保证都不适用于非final 字段的写入。

然而,一个我没有看到答案的问题是:如果一个类的语义使得一个字段不能是final,但希望确保该字段和由此识别的对象的“发布” ,最有效的方法是什么?例如,考虑

class ShareableDataHolder<T>
{
  Object data; // Always identifies either a T or a SharedDataHolder<T>
}
private class SharedDataHolder<T> extends ShareableDataHolder<T>
{
  Object data; // Always identifies either a T or a lower-numbered SharedDataHolder<T>
  final long seq; // Immutable; necessarily unique
}

意图是data 最初将直接标识一个数据对象,但它可以在任何时候合法地更改为标识一个直接或间接封装等效数据对象的SharedDataHolder&lt;T&gt;。如果对data 的任何读取可能会任意返回曾经写入data 的任何值,但如果读取null 则可能会失败,则假设所有代码都已写入正常工作(尽管不一定以最佳效率)。

声明volatile Object data 在语义上是正确的,但可能会对随后对该字段的每次访问产生额外成本。在最初设置字段后输入虚拟锁会起作用,但会不必要地慢。有一个虚拟的final 字段,对象设置它来标识自己似乎应该可以工作;尽管从技术上讲,我认为可能需要通过另一个字段完成对另一个字段的所有访问,但我看不到任何重要的现实场景。在任何情况下,拥有一个目的只是通过其存在提供适当同步的虚拟字段似乎是一种浪费。

是否有任何干净的方法来通知编译器在构造函数中对data 的特定写入应该与在构造函数返回后发生的对该字段的任何读取具有发生之前的关系(就像这种情况一样如果字段为final),而无需支付与volatile、锁等相关的费用?或者,如果一个线程要读取data 并发现它为空,它是否可以以某种方式重复读取以建立关于data 写入的“发生之后”[认识到这样的请求可能很慢,但不需要经常发生]?

PS--如果happens-before关系是不可传递的,那么在以下场景中是否存在适当的happens-before关系?

  1. 线程 1 写入某个对象 Fred 中的非最终字段 dat,并将对它的引用存储到最终字段 George
  2. 线程 2 将引用从 George 复制到非最终字段 Larry
  3. 线程 3 读取 Larry.dat

据我所知,Fred 的字段dat 的写入和George 的读取之间存在发生前的关系。 Fred 的dat 的写入和Larry 的读取之间是否存在happens-before 关系,后者返回从final 引用复制到Fred 的对Fred 的引用?如果没有,是否有任何“安全”的方法可以将 final 字段中包含的引用复制到可通过其他线程访问的非最终字段?

PPS--如果在主构造函数完成之前从未在其创建线程之外访问对象及其组成部分,并且主构造函数的最后一步是在主对象中存储一个 final 对其自身的引用,是否存在任何“合理的”实现/场景,其中另一个线程可以看到部分构造的对象,无论是否有任何东西实际使用该 final 引用?

【问题讨论】:

  • " 他们还指出,在最终字段中存储对外部线程从未访问过的可变对象的引用将确保在存储之前对该对象进行的所有更改在通过该字段访问对象的所有线程上可见"
  • 另外:“声明 volatile 对象数据在语义上是正确的,但可能会在每次后续访问该字段时产生额外成本。” volatile 字段贵。 More info
  • 问题是两个不相关的特性——不可重新分配,可见性——被同一个关键字final不必要地耦合。

标签: java thread-safety immutability


【解决方案1】:

简答

没有。

更长的答案

JLS 17.4.5 列出了所有* 建立前发生关系的方式,final 字段语义的特殊情况除外:

  1. 监视器上的解锁发生在该监视器上的每个后续锁定之前。
  2. 对 volatile 字段(第 8.3.1.4 节)的写入发生在对该字段的每次后续读取之前。
  3. 线程上的 start() 调用发生在已启动线程中的任何操作之前。
  4. 线程中的所有操作都发生在任何其他线程从该线程上的 join() 成功返回之前。
  5. 任何对象的默认初始化发生在程序的任何其他操作(默认写入除外)之前。

(原文将它们列为要点;为方便起见,我将它们更改为数字。)

现在,您已经排除了锁 (#1) 和 volatile 字段 (#2)。规则 #3 和 #4 与线程的生命周期有关,您在问题中没有提到,听起来也不适用。规则 #5 不会为您提供任何非null 值,因此它也不适用。

因此,在建立happens-before 的五种可能方法中,除了final 字段语义之外,三种不适用,两种您已明确排除。


* 17.4.5 中列出的规则实际上是 17.4.4 中定义的同步顺序规则的结果,但这些规则与 17.4.5 中提到的规则直接相关。我提到,因为 17.4.5 的列表可以解释为说明性的,因此不是详尽的,但 17.4.4 的列表不是说明性的和详尽的,如果您不想这样做,您可以直接从中进行相同的分析依赖 17.4.5 提供的中间分析。

【讨论】:

  • 如果构造函数的最后一步导致对构造对象的引用被存储到某个对象的最终字段中,那么“全局”是否会建立在此之前写入对象的所有内容的顺序时间,还是仅针对实际读取该特定 final 字段的代码建立排序?如果语义如所述,上述类的最合理实现是什么?也许volatile 会是最好的方法,但代码要求 JVM 提供比它需要的更强大的保证似乎很难看。
  • hb 排序仅针对读取特定的final 字段。有人谈到向 Java 添加一个栅栏 API,它可以提供您所指的更精细的语义,但没有成功。
  • 如果线程读取final字段的内容并将其复制到非最终字段,则适用于final字段的happens-before关系也适用于复制的字段?否则,您会如何看待拥有两个data 字段、一个易失性和一个非易失性字段的想法?对于许多读取操作,与陈旧数据相关的成本将低于让 CPU 确保数据是最新的成本,但是如果从非易失性指针读取 null 的代码要读取易失性指针,保证正确性。
  • 另一种方法是让每个对象对其提供的数据使用final 字段,与标识共享数据链接的字段分开。但是,这样做会失去丢弃冗余信息的能力。
  • final 字段的 hb 关系没有传递性,所以我认为复制方法不起作用;重新排序可能会给您带来奇怪的结果。两引用方法可行,但您需要确保即使在没有同步的情况下,所讨论的对象也是线程安全的——这基本上意味着该对象中的所有内容都必须是 final
【解决方案2】:

您可以应用 final 字段语义,而无需将类的字段设为 final,而是通过另一个 final 字段传递您的引用。为此,您需要定义一个发布者类:

class Publisher<T> {
  private final T value;
  private Publisher(T value) { this.value = value; }
  public static <S> S publish(S value) { return new Publisher<S>(value).value; }
}

如果您现在正在使用ShareableDataHolder&lt;T&gt; 的实例,您可以通过以下方式发布该实例:

ShareableDataHolder<T> holder = new ShareableDataHolder<T>();
// set field values
holder = Publisher.publish(holder);
// Passing holder to other threads is now safe

这种方法是tested and benchmarked,事实证明它是当前虚拟机上性能最高的替代方案。开销很小,因为逃逸分析通常会移除非常短暂的 Publisher 实例的分配。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-04-11
    • 1970-01-01
    • 1970-01-01
    • 2012-02-19
    • 1970-01-01
    • 1970-01-01
    • 2015-06-01
    • 1970-01-01
    相关资源
    最近更新 更多