番石榴中使用的无锁延迟加载模式真的是线程安全的吗?

一些番石榴内部类型,如 ,有这样的模式:AbstractMultiset

private transient Set<E> elementSet;

@Override
public Set<E> elementSet() {
  Set<E> result = elementSet;
  if (result == null) {
    elementSet = result = createElementSet();
  }
  return result;
}

Set<E> createElementSet() {
  return new ElementSet();
}

这个想法是延迟创建集合视图(,),直到实际需要它们。进程周围没有锁定,因为如果两个线程同时调用,则可以返回两个不同的值。编写字段将会有一场竞赛,但是由于写入引用字段在Java中始终是原子的,因此谁赢得比赛并不重要。elementSet()entrySet()elementSet()elementSet

但是,我担心Java内存模型在这里对内联有什么看法。如果 和 的构造函数都内联,似乎我们可以得到这样的东西:createElementSet()ElementSet

@Override
public Set<E> elementSet() {
  Set<E> result = elementSet;
  if (result == null) {
    elementSet = result = (allocate an ElementSet);
    (run ElementSet's constructor);
  }
  return result;
}

这将允许另一个线程观察 的非空值,但初始化不完全。有没有不发生的原因?从我对 JLS 17.5 的阅读来看,似乎其他线程只能保证看到 中字段的正确值,但由于最终派生自 ,我不认为可以保证它的所有字段都是 。elementSetfinalelementSetElementSetAbstractSetfinal


答案 1

我不是100%清楚这一点(我相信我们团队中的其他人可以更好地回答这个问题)。也就是说,有几个想法:

  1. 我不认为我们在任何地方声称这是(保证)线程安全的。非线程安全集合,如 extend 。也就是说,还扩展并使用了它的实现,因此大概它实际上必须是线程安全的。HashMultisetAbstractMultisetConcurrentHashMultisetAbstractMultisetelementSet()
  2. 我相信这种方法的线程安全性取决于.据我所知,如果创建的 by 是不可变的(因为在构造时分配的字段是 ),它应该是线程安全的。至少在以下情况下,这似乎是正确的。createElementSet()SetcreateElementSet()finalConcurrentHashMultiset

编辑:我问杰里米·曼森这件事,他说:“你对这件事的看法对我来说似乎很好。它不是线程安全的。如果正在构造的对象在正确的位置具有所有最终字段,那么您应该没问题,但我不会偶然地依赖它(请注意,许多实现实际上是不可变的,而不是真正不可变的)。

注意:对于像使用此模式的线程安全集合,创建的对象是有意的和真正的不可变的(尽管如果要更改,可能会更改,正如 Chris 在注释中指出的那样)。ConcurrentHashMultisetAbstractSet


答案 2

推荐