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

Ada*_*m R 16 java multithreading

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

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

biz*_*lop 13

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

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

public ListInConstructor()
{
    List<Object> tmp = new ArrayList<>();
    tmp.add(new Object());
    this.list = tmp;
}
Run Code Online (Sandbox Code Playgroud)

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

17.5.最终的场语义学

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

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

  • @biziclop问题是,谁可以回答这个问题的话,如果我们没有得到幸运和Brian或有人走来:-)我做了我的解释添加在任何情况下的问题,但它肯定是一个棘手的情况下! (2认同)

Joh*_*int 5

根据JLS

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

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

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

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

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

我将此解释为修改数组的操作发生 - 在后面的derefencing之前r2是冻结完成后的非同步读取(构造函数存在).


Voo*_*Voo 2

好的,这就是 JLS \xc2\xa717.5.1关于该主题的说法。

\n\n

首先:

\n\n
\n

设 o 是一个对象,c 是 o 的构造函数,其中写入了 Final\n 字段 f。当 c 正常或突然退出时,会对 o 的最后一个字段 f 执行冻结操作

\n
\n\n

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

\n\n
public ListInConstructor() {\n    list = new ArrayList<>();\n    list.add(new Object());\n} // the freeze action happens here!\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在有趣的部分是:

\n\n
\n

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

\n
\n\n

所以让我们一次做一件:

\n\n

我们有 hb(w,f),这意味着我们最终字段。

\n\n

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

\n\n

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

\n\n

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

\n\n

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

\n\n

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

\n