[英]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.