【发布时间】:2017-12-30 06:59:04
【问题描述】:
我想创建一个给定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 进行易失性读取,每个竞争线程最多只进入 synchronized 块一次,而 result 未初始化,然后再也不会.
但是一旦result 被初始化,是否有可能通过首先检查非易失性标志initialized 和short- 在随后的调用中避免delegate 的非同步易失性读取的成本,但可以忽略不计电路?或者这对我来说完全没有正常的双重检查锁定?或者它是否以某种方式对性能的伤害大于它的帮助?还是真的坏了?
【问题讨论】:
-
是的。您可以删除
supplier检查并仅检查initialized。然后在同步块中将supplier设置为空,因为之后它将不再使用。这将与 Guava 的Suppliers.memoize相同,只是允许对委托进行 GC。 -
@BenManes 我看到了源代码,但他们的
initialized标志是volatile,所以他们仍然在每次调用时进行易失性读取。我很好奇,一旦result被初始化,是否也可以避免这种情况。我认为我的方法有问题,我只是看不到它,因为我对 Java 的内存模型了解不够。 -
A
Supplier可以返回null结果,因此如果不检查 volatile 标志,就无法确定您是否进行了激烈的读取,这将充当内存屏障确保一致性。但是 volatile 读取在 x86 上大部分是免费的,因此即使您不安全地放松它,您也不会在 JMH 基准测试中看到收益。 -
@BenManes 是的,我想说明返回
null的供应商,这就是我捎带原始供应商的原因。供应商是易变的,而不是结果,我正在检查供应商是否为空,确保它在构造函数中不为空,并在同步块的末尾将其清空(使其有资格作为 GC小红利)。非易失性initialized字段用于避免随后对原始供应商进行易失性读取,因为我承认这可能是微不足道的收益。我只是想知道它是否真的有效。
标签: java multithreading concurrency thread-safety double-checked-locking