我想创建一个给定Supplier
的记忆版本,以便多个线程可以同时使用它,同时保证原始供应商的get()
最多被调用一次,并且所有线程都看到相同的结果。双重检查锁定似乎很合适。
class CachingSupplier<T> implements Supplier<T> {
private T result = null;
private boolean initialized = false;
private volatile Supplier<? extends T> delegate;
CachingSupplier(Supplier<? extends T> delegate) {
this.delegate = Objects.requireNonNull(delegate);
}
@Override
public T get() {
if (!this.initialized && this.delegate != null) {
synchronized (this) {
Supplier<? extends T> supplier = this.delegate;
if (supplier != null) {
this.result = supplier.get();
this.initialized = true;
this.delegate = null;
}
}
}
return this.result;
}
}
我的理解是,在这种情况下,delegate
需要volatile
,否则synchronized
块中的代码可能会重新排序:写入delegate
可能发生在写入result
之前,可能会在完全初始化之前将result
暴露给其他线程。这是对的吗?
因此,通常这需要在每次调用时在synchronized
块之外对delegate
进行易失性读取,在result
未初始化时,每个竞争线程最多只进入一次synchronized
块,然后再也不会。
但是,一旦初始化了result
,是否也可以通过首先检查非易失性标志initialized
和短路来避免后续调用时delegate
的不同步易失性读取的成本,无论多么微不足道?还是这绝对比正常的双重检查锁定给我买不到什么?还是它以某种方式损害性能大于帮助?还是真的坏了?
不要实现双重检查锁定,请使用为您完成工作的现有工具:
class CachingSupplier<T> implements Supplier<T> {
private final Supplier<? extends T> delegate;
private final ConcurrentHashMap<Supplier<? extends T>,T> map=new ConcurrentHashMap<>();
CachingSupplier(Supplier<? extends T> delegate) {
this.delegate = Objects.requireNonNull(delegate);;
}
@Override
public T get() {
return map.computeIfAbsent(delegate, Supplier::get);
}
}
请注意,通常情况下,简单地进行一次急切的首次评估,并在将供应商发布到其他线程之前将其替换为不断返回的供应商,这甚至更简单和足够。或者只是使用volatile
变量并接受如果多个线程遇到尚未评估的供应商,可能会有一些并发评估。
下面的实现仅用于信息(学术)目的,强烈建议使用上述更简单的实现。
您可以改用不可变对象的发布保证:
class CachingSupplier<T> implements Supplier<T> {
private Supplier<? extends T> delegate;
private boolean initialized;
CachingSupplier(Supplier<? extends T> delegate) {
Objects.requireNonNull(delegate);
this.delegate = () -> {
synchronized(this) {
if(!initialized) {
T value = delegate.get();
this.delegate = () -> value;
initialized = true;
return value;
}
return this.delegate.get();
}
};
}
@Override
public T get() {
return this.delegate.get();
}
}
在这里,initialized
是在synchronized(this)
保护下写入和读取的,但在第一次计算时,delegate
被一个新的Supplier
所取代,该总是返回评估值,而无需进行任何检查。
由于新供应商是不可变的,因此即使由从未执行过synchronized
块的线程读取,它也是安全的。
正如 igaz 正确指出的那样,如果CachingSupplier
实例本身没有安全发布,则上面的类不能免受数据争用的影响。一个完全不受数据争用影响的实现,即使发布不当,但在普通访问情况下仍然可以在没有内存障碍的情况下工作,甚至涉及更多:
class CachingSupplier<T> implements Supplier<T> {
private final List<Supplier<? extends T>> delegate;
private boolean initialized;
CachingSupplier(Supplier<? extends T> delegate) {
Objects.requireNonNull(delegate);
this.delegate = Arrays.asList(() -> {
synchronized(this) {
if(!initialized) {
T value = delegate.get();
setSupplier(() -> value);
initialized = true;
return value;
}
return getSupplier().get();
}
});
}
private void setSupplier(Supplier<? extends T> s) {
delegate.set(0, s);
}
private Supplier<? extends T> getSupplier() {
return delegate.get(0);
}
@Override
public T get() {
return getSupplier().get();
}
}
我认为这更加强调了第一个解决方案的美感......
它被破坏了,即它不是多线程安全的。 根据 JMM 的说法,简单地"看到"共享内存值(在您的示例中,读取器线程可能会看到 #initialized 为真)不是发生之前的关系,因此读取器线程可以:
load initialized //evaluates true
load result //evaluates null
以上是允许的执行。
没有办法避免同步操作的"成本"(例如,易失性写入的易失性读取),同时避免数据争用(以及随之而来的损坏代码)。句点。
概念上的困难在于打破常识推理,即要使线程看到初始化为 true,>必须先写 true 才能初始化;尽管很难接受,但该推断是不正确的
正如Ben Manes指出的那样,易失性读取只是x-86上的普通负载
。