建立最便宜的方式发生在非最终领域之前

时间:2014-03-21 17:10:07

标签: java thread-safety immutability

许多问题/答案表明,如果一个类对象有一个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<T>。如果data的任何读取可以任意返回曾写入data的任何值,则假设所有代码都被正确编写(尽管不一定非常有效),但如果读取null则可能会失败}}

声明volatile Object data在语义上是正确的,但可能会在每次后续访问该字段时增加额外费用。在最初设置字段后输入虚拟锁会起作用,但会不必要地慢。有一个虚拟的final字段,该对象设置为标识自己似乎应该工作;虽然从技术上讲,我认为可能需要通过其他字段完成对其他字段的所有访问,但我看不出任何真实的情况。在任何情况下,拥有一个虚拟字段,其目的只是通过其存在提供适当的同步,这似乎是浪费。

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

PS - 如果在关系不可传递之前发生,那么在以下场景中存在关系之前是否会发生正确的事情?

  1. 线程1写入某个对象dat中的非最终字段Fred,并将对其的引用存储到最终字段George
  2. 主题2将引用从George复制到非最终字段Larry
  3. 主题3读取Larry.dat
  4. 据我所知,Fred的字段dat的写入与George的读取之间存在先发生过的关系。在Fred dat的写入和Larry 的读取之间存在关系,它会返回对Fred的引用,该引用从final引用复制到{{ 1}} ?如果没有,是否有任何“安全”的方法将Fred字段中包含的引用复制到可通过其他线程访问的非最终字段?

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

2 个答案:

答案 0 :(得分:6)

简短回答

没有

更长的答案

除了final字段语义的特殊情况之外,

JLS 17.4.5列出了建立先发生关系的所有方法:

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

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

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

因此,除了final字段语义之外,有五种可能的方法用于建立先发生的事件,其中三种不适用,另外两种已明确排除。


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

答案 1 :(得分:0)

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

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<T>的实例,则可以通过以下方式发布实例:

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实例的分配,因此开销很小。