[英]Avoiding volatile reads in thread-safe memoizing Supplier
我想创建给定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
的非易失性标志和短路,来避免在随后的调用中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
代替,该Supplier
始终返回评估值而无需任何检查。
由于新的供应商是不可变的,因此即使被从未执行过synchronized
块的线程读取,它也是安全的。
正如igaz正确指出的,如果CachingSupplier
实例本身未安全发布,则上面的类不能不受数据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为true),这不是事前发生的关系,因此读取器线程可以:
load initialized //evaluates true
load result //evaluates null
以上是允许的执行。
无法避免同步操作的“成本”(例如,易失性写的易失性读取),同时又避免了数据争用(并因此破坏了代码)。 句号
在概念上的困难是打破常理推断,对于一个线程看到初始化为真- >必须有真正到初始化之前写; 很难接受,推断是不正确的
正如Ben Manes所指出的那样,易失性读取只是x-86上的旧负载
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.