【问题标题】:Does mutation of an non-thread-safe collection in a constructor need to be synchronized?构造函数中非线程安全集合的突变是否需要同步?
【发布时间】:2015-11-18 16:35:29
【问题描述】:

如果我决定使用非线程安全集合并同步其访问,我是否需要同步构造函数中的任何突变?例如,在下面的代码中,我理解对列表的引用将在构造后对所有线程可见,因为它是最终的。但是我不知道这是否构成安全发布,因为构造函数中的 add 是不同步的,它是在 ArrayList 的 elementData 数组中添加一个引用,这是非最终的。

private final List<Object> list;

public ListInConstructor()
{
    list = new ArrayList<>();
    // synchronize here?
    list.add(new Object());
}

public void mutate()
{
    synchronized (list)
    {
        if (list.checkSomething())
        {
            list.mutateSomething();
        }
    }
}

【问题讨论】:

  • 您是否以任何方式同步初始化/访问ListInConstructor 实例?
  • 不,我不会以任何方式同步 ListInConstructor 对象,但如果有影响,它会存储在 final 字段中。

标签: java multithreading


【解决方案1】:

更新:Java 语言规范声明 冻结 使更改可见 必须在构造函数的末尾,这意味着您的代码是 正确同步,请参阅John Vint 的答案和 Voo.

但是你也可以这样做,这绝对有效:

public ListInConstructor()
{
    List<Object> tmp = new ArrayList<>();
    tmp.add(new Object());
    this.list = tmp;
}

在这里,我们将列表对象分配给final 字段之前对其进行变异,因此分配将保证对列表所做的任何更改也将可见。

17.5. final Field Semantics

final字段的使用模型很简单:设置final字段 对于该对象的构造函数中的对象;并且不要写 对在另一个地方构造的对象的引用 线程可以在对象的构造函数完成之前看到它。如果这 紧随其后,然后当另一个线程看到该对象时, 线程将始终看到正确构造的版本 对象的最终字段。 它还会看到任何对象的版本或 由那些至少是最新的最终字段引用的数组 就像最后的字段一样。

突出显示的句子向您保证此解决方案将起作用。虽然,正如答案开头所指出的那样,原件也必须工作,但我将把这个答案留在这里,因为规范有点令人困惑。而且因为这个“技巧”在设置非最终但 volatile 字段时也有效(来自任何上下文,而不仅仅是构造函数)。

【讨论】:

  • 你能解释一下为什么你把引用的那部分加粗了吗?
  • @SotiriosDelimanolis 在这个确切的时刻,我真的不了解自己。这句话对我来说意味着它们将与设置字段时一样最新,但下一节将讨论在构造函数末尾冻结。
  • 粗体部分不适用于此处。也就是说,最终字段对象的构造函数在某处引用了全局字段。如果约翰可以取消删除他的问题并添加 17.5.1 中给出的解释?我今晚可能会忘记写答案,而且我肯定不会在手机上写出好的答案:-)
  • 考虑到我们不太确定,我将取消删除我的答案
  • @biziclop 问题是谁能回答那个问题,如果我们不走运并且布赖恩或其他人出现:-) 无论如何我确实在问题中添加了我的解释,但它肯定是一个棘手的案例!
【解决方案2】:

根据JLS

final 字段的使用模型很简单:在对象的构造函数中设置对象的 final 字段;并且不要在对象的构造函数完成之前在另一个线程可以看到它的地方写入对正在构造的对象的引用。

由于对 List 的写入发生在构造函数完成之前,您可以安全地改变列表而无需额外同步。

编辑:根据 Voo 的评论,我将进行编辑,包括最终字段冻结。

所以阅读更多 17.5.1 有这个条目

给定一个写入 w、一个冻结 f、一个动作 a(不是读取最终字段)、读取被 f 冻结的最终字段的 r1 和读取 r2,使得 hb(w, f) , hb(f, a), mc(a, r1) 和解引用(r1, r2),

我将此解释为修改数组的操作发生在 r2 的后续取消引用之前,这是冻结完成后的非同步读取(构造函数存在)。

【讨论】:

    【解决方案3】:

    好的,这就是 JLS §17.5.1 在这个话题上要说的。

    首先:

    令 o 为对象,c 为 o 的构造函数,其中一个 final 字段 f 被写入。 在 o 的最后一个字段 f 上发生冻结动作 当 c 正常或突然退出时

    所以我们知道在我们的代码中:

    public ListInConstructor() {
        list = new ArrayList<>();
        list.add(new Object());
    } // the freeze action happens here!
    

    所以现在有趣的部分:

    给定一个写入 w,一个冻结 f,一个动作 a(不是读取 a 最终字段),由 f 冻结的最终字段的读取 r1,以及读取 r2 这样 hb(w, f)、hb(f, a)、mc(a, r1) 和 dereferences(r1, r2), 然后在确定 r2 可以看到哪些值时,我们考虑 hb(w, r2)。

    让我们一次做一件:

    我们有 hb(w,f),这意味着我们在离开构造函数之前写入到 final 字段。

    r1 是读取最终字段和取消引用(r1, r2)。这意味着 r1 读取最终字段,然后 r2 读取该最终字段的某个值。

    我们还有一个动作(读取或写入,但不是读取最终字段),它具有 hb(f,a) 和 mc(a, r1)。这意味着该动作发生在构造函数之后,但之后可以被读取的 r1 看到。

    因此它声明“我们考虑 hb(w, r2)”,这意味着写入必须发生在读取使用 r1 读取的最终字段的值之前。

    所以在我看来,很明显添加到列表中的对象必须对任何可以读取list 的线程可见。

    附注:HotSpot 通过在任何包含 final 字段的构造函数的末尾放置一个内存屏障来实现 final 字段语义,从而在任何情况下都保证此属性。这是否只是一种优化(最好只对单个屏障进行,并且尽可能远离写入)是另一个问题。

    【讨论】:

      【解决方案4】:

      由于对象本身不是不可变的,因此您必须安全地发布该对象。只要您这样做,就无需在 synchronized 块中的构造函数中进行突变。

      可以通过多种方式“安全地发布”对象。一个例子是通过正确同步的队列将它们传递给另一个线程。有关更多详细信息,请参阅 Java 并发实践第 3.5.3 节“安全发布习惯用法”和 3.5.4“有效不可变对象”

      【讨论】:

      • 通过“安全地发布对象”,我们正在谈论同步访问/初始化ListInConstructor 实例,对吧?
      • @SnildDolkow 或将其设置为 volatilefinal 字段的值,这保证对象将至少与写入变量。
      • 这在任何情况下都是完全安全的(在将实例分配给非最终、非易失性字段时也是如此)。他将看到前一个对象,或者如果他看到新创建的对象,他保证会看到完全构造的值,因为它是最终的(他不会看到的唯一方法是在构造函数完成之前泄漏 this 引用)
      • @Voo 保证你会看到一个正确构造的ArrayList,但它是否包含额外的元素不是。
      • @biziclop 不是我阅读 17.5.1 的方式:“当 c 正常或突然退出时,会在 o 的最后一个字段 f 上发生冻结操作。”写入 f 时不会发生冻结动作。然后根据 f 定义 hb 关系,所以这对我来说似乎是安全的。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-12-19
      • 2013-02-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-11-16
      • 2011-09-16
      相关资源
      最近更新 更多