我正在寻找与 Is it appropriate to use AtomicReference.compareAndSet to set a reference to the results of a database call? 类似但要求不同的问题的答案。

目标是只创建一次 ObjectWithSideEffectConstructor 的实例以避免重复的副作用。构造必须发生在 setUp() 中。多个线程将调用 setUp() 。同样会有一个tearDown()用于从对象中回收资源,这里省略。问题:实现目标的最佳实践是什么?

仅仅使用 AtomicReference 是不够的,因为会先执行构造函数,所以会产生副作用。

private static AtomicReference<ObjectWithSideEffectConstructor> ref =
  new AtomicReference<ObjectWithSideEffectConstructor>()

void setUp() {
  ref.compareAndSet(null, new ObjectWithSideEffectConstructor());
}

使用 Is it appropriate to use AtomicReference.compareAndSet to set a reference to the results of a database call? 的答案是行不通的,因为 volatile 缺乏同步。会有多个线程进入 if 的窗口。
private static volatile ObjectWithSideEffectConstructor obj;

void setUp() {
  if (obj == null) obj = new ObjectWithSideEffectConstructor();
}

简单的修复是
private static ObjectWithSideEffectConstructor obj;
private static final Object monitor = new Object();

void setUp() {
  synchronized (monitor) {
    if (obj == null) obj = new ObjectWithSideEffectConstructor();
  }
}

同样,带有 volatile 监视器的 DCL 可能会提供更好的读取性能。但是两者都需要一定程度的同步,因此预计性能会更差。

我们也可以使用 FutureTask 。它更高效,因为一旦创建了对象,后续的 FutureTask.get() 将无阻塞地返回。但它肯定比 synchronized 复杂得多。
private static final AtomicReference<FutureTask<ObjectWithSideEffectConstructor>> ref =
  new AtomicReference<FutureTask<ObjectWithSideEffectConstructor>>();

void setUp() {
  final FutureTask<ObjectWithSideEffectConstructor> future =
    new FutureTask<ObjectWithSideEffectConstructor>(
      new Callable<ObjectWithSideEffectConstructor>() {
        @Override
        public ObjectWithSideEffectConstructor call() throws InterruptedException {
          return new ObjectWithSideEffectConstructor();
        }
      }
    );
  if (ref.compareAndSet(null, future)) future.run();
  ref.get().get();
}

感谢您的建议。

最佳答案

我假设您只需要一个 ObjectWithSideEffectConstructor。这里有一个问题,即 1) 这是您想要避免的副作用发生两次,还是 2) 您只需要最终获得一致的(单例)引用。

无论哪种方式,synchronized 都是一个很好的标准选项。它将阻止其他线程构建第二个实例,而第一个线程正在设置中。

如果您处于情况 1),则可能需要使用 synchronized。如果启动后的性能很重要,您可以考虑在同步部分之前使用 AtomicReference.get() 快速路径,以便在启动完成后避免同步部分。

如果您处于情况 2),那么 - 从您的问题中并不清楚 - 构造的副作用,但您不关心复制它 - 只要客户端代码只有“看到”一致的单一引用。

在第二种情况下,您可以使用 AtomicReference.get() 检查它是否已初始化,如果已初始化则返回。然后线程将进入“竞争部分”,在那里它们将构造(可能是多个)ObjectWithSideEffectConstructor。最后,会有一个 compareAndSet 以便只有一个线程设置单例......失败的线程会回退到 AtomicReference.get() 以获取正确的单例。

在性能方面,对 AtomicReference 的单次调用比 synchronized 块快——但我不确定,通过双重和三重检查和构造不需要的副作用对象,第二种方法是否是。同样,一个简单的 synchronized 块可能更简单、更快。

我很想看看一些测量结果。

关于java - 在并发环境中创建单例的最佳实践?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/19328989/

10-10 20:23