是否需要同步构造函数中非线程安全集合的突变?

2022-09-03 06:12:54

如果我决定使用非线程安全集合并同步其访问,是否需要同步构造函数中的任何突变?例如,在下面的代码中,我理解对列表的引用将在构造后对所有线程可见,因为它是最终的。但我不知道这是否构成安全发布,因为构造函数中的 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();
        }
    }
}

答案 1

更新:Java语言规范规定,使更改可见的冻结必须在构造函数的末尾,这意味着您的代码已正确同步,请参阅John VintVoo的答案。

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

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

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

17.5. 最终的字段语义

最终字段的使用模型很简单:在对象的构造函数中设置对象的最终字段;并且不要在对象的构造函数完成之前,在另一个线程可以看到它的位置写入对正在构造的对象的引用。如果遵循此命令,则当另一个线程看到该对象时,该线程将始终看到该对象的最终字段的正确构造版本。它还将看到由最终字段引用的任何对象或数组的版本,这些字段至少与最终字段一样最新。

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


答案 2

根据JLS

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

由于写入 List 是在构造函数完成之前进行的,因此您可以安全地更改列表,而无需进行其他同步。

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

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

给定一个写 w、一个冻结 f、一个动作 a(不是对最终场的读取)、一个被 f 冻结的最终字段的读取 r1,以及一个读取 r2,使得 hb(w, f), hb(f, a)、mc(a, r1) 和 dereferences(r1, r2),

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