我正在寻找与 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/