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

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

private final List list; public ListInConstructor() { list = new ArrayList(); // synchronize here? list.add(new Object()); } public void mutate() { synchronized (list) { if (list.checkSomething()) { list.mutateSomething(); } } } 

好的,这就是JLS§17.5.1对该主题所说的内容。

首先:

设o为对象,c为o的构造函数,其中写入最终字段f。 当c退出时,正常或突然发生对o的最终场f的冻结动作

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

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

所以现在有趣的部分:

给定写入w,冻结f,动作a(不是最终字段的读取),由f冻结的最终字段的读取r1,以及读取r2使得hb(w,f),hb( f,a),mc(a,r1)和解引用(r1,r2),然后当确定r2可以看到哪些值时,我们考虑hb(w,r2)。

所以让我们一次做一件事:

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

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

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

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

所以我看到它的方式很明显,任何可以读取list线程都必须能够看到添加到列表中的对象。

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

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

但是你也可以这样做,这肯定有效:

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

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

17.5。 最终的场语义学

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

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

根据JLS

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

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

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

因此,阅读更多内容到17.5.1就有这个条目

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

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

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

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