[英]Atomic compareAndSet but with callback?
我知道AtomicReference
有compareAndSet
,但我覺得我想做的是這個
private final AtomicReference<Boolean> initialized = new AtomicReference<>( false );
...
atomicRef.compareSetAndDo( false, true, () -> {
// stuff that only happens if false
});
這可能也有效,可能會更好。
atomicRef.compareAndSet( false, () -> {
// stuff that only happens if false
// if I die still false.
return true;
});
我注意到有一些新的功能結構,但我不確定它們中是否有任何一個是我正在尋找的。
任何新的構造都可以做到這一點嗎? 如果是這樣,請提供一個例子。
更新為了簡化我的問題,我試圖找到一種更不容易出錯的方法來保護代碼,以“為對象做一次”或(真的)懶惰的初始化方式,我知道我團隊中的一些開發人員發現compareAndSet
令人困惑.
“為對象做一次”中的保護代碼
如何具體實現取決於您希望其他線程同時嘗試執行相同的操作。 如果您只是讓它們通過 CAS,它們可能會在中間狀態下觀察事物,而成功的一個線程會執行其操作。
或(真的)懶惰的初始化方式
如果您將其用於惰性初始化程序,則該構造不是線程安全的,因為“已初始化”布爾值可能會被一個線程設置為 true,然后在另一個線程觀察到 true 狀態但讀取一個空結果時執行該塊。
如果可以接受多個並發/重復初始化嘗試,並且一個對象最終獲勝而其他對象被 GC 丟棄,則可以使用Atomicreference::updateAndGet 。 更新方法應該是無副作用的。
否則,您應該只使用帶有變量引用字段的雙重檢查鎖定模式。
當然,您始終可以將這些中的任何一個打包到一個高階函數中,該函數返回一個Runnable
或Supplier
,然后您將其分配給最終字段。
// == FunctionalUtils.java
/** @param mayRunMultipleTimes must be side-effect-free */
public static <T> Supplier<T> instantiateOne(Supplier<T> mayRunMultipleTimes) {
AtomicReference<T> ref = new AtomicReference<>(null);
return () -> {
T val = ref.get(); // fast-path if already initialized
if(val != null)
return val;
return ref.updateAndGet(v -> v == null ? mayRunMultipleTimes.get() : v)
};
}
// == ClassWithLazyField.java
private final Supplier<Foo> lazyInstanceVal = FunctionalUtils.instantiateOne(() -> new Foo());
public Foo getFoo() {
lazyInstanceVal.get();
}
您可以通過這種方式輕松封裝各種自定義控制流和鎖定模式。 這是我自己的兩個。 .
如果更新完成, compareAndSet
返回 true,如果實際值不等於預期值,則返回 false。
所以只需使用
if (ref.compareAndSet(expectedValue, newValue)) {
...
}
也就是說,我不太理解您的示例,因為您將 true 和 false 傳遞給將對象引用作為參數的方法。 你的第二個例子與第一個例子不同。 如果第二個是你想要的,我想你所追求的是
ref.getAndUpdate(value -> {
if (value.equals(expectedValue)) {
return someNewValue(value);
}
else {
return value;
}
});
你把事情復雜化了。 僅僅因為現在有 lambda 表達式,你不需要用 lambdas 解決所有問題:
private volatile boolean initialized;
…
if(!initialized) synchronized(this) {
if(!initialized) {
// stuff to be done exactly once
initialized=true;
}
}
雙重檢查鎖定可能沒有很好的聲譽,但對於非static
屬性,幾乎沒有其他選擇。
如果您考慮多個線程在未初始化狀態下並發訪問它,並希望保證該操作僅運行一次,並且在執行相關代碼之前已完成,則Atomic…
對象將無濟於事。
只有一個線程可以成功執行compareAndSet(false,true)
,但由於失敗意味着標志已經具有新值,即已初始化,所有其他線程將繼續進行,就好像“只需要完成一次的事情”已經完成在它可能仍在運行時完成。 另一種方法是先讀取標志,然后有條件地執行這些內容,然后再進行compareAndSet
,但這允許“東西”的多個並發執行。 這也是updateAndGet
或accumulateAndGet
發生的情況,它提供了函數。
為了保證在繼續之前只執行一次,如果“東西”當前正在執行,線程必須被阻塞。 上面的代碼就是這樣做的。 請注意,一旦“東西”完成,將不再有鎖定,並且volatile
讀取的性能特征與Atomic…
讀取相同。
編程中更簡單的唯一解決方案是使用ConcurrentMap
:
private final ConcurrentHashMap<String,Boolean> initialized=new ConcurrentHashMap<>();
…
initialized.computeIfAbsent("dummy", ignore -> {
// stuff to do exactly once
return true;
});
它可能看起來有點過大,但它確實提供了所需的性能特征。 它將使用synchronized
(或者說,依賴於實現的排除機制)保護初始計算,但在后續查詢中執行具有volatile
語義的單次讀取。
如果您想要一個更輕量級的解決方案,您可以繼續使用本答案開頭顯示的雙重檢查鎖定......
我知道這已經過時了,但我發現沒有完美的方法來實現這一點,更具體地說:
試圖找到一種不太容易出錯的方法來保護“做(任何事情)一次……”中的代碼
我將補充這一點“同時尊重發生在行為之前。” 這是在您的情況下實例化單例所必需的。
IMO 實現此目的的最佳方法是通過同步函數:
public<T> T transaction(Function<NonSyncObject, T> transaction) {
synchronized (lock) {
return transaction.apply(nonSyncObject);
}
}
這允許在給定對象上執行原子“事務”。
其他選項是雙重檢查自旋鎖:
for (;;) {
T t = atomicT.get();
T newT = new T();
if (atomicT.compareAndSet(t, newT)) return;
}
在這個new T();
將重復執行,直到成功設置值,所以它不是真正的“做一次”。
這僅適用於寫入事務的復制,並且可以通過調整代碼來幫助“實例化對象一次”(實際上是實例化許多但最終引用相同的對象)。
最后一個選項是第一個選項的性能最差的版本,但這個版本是在 AND ONCE 之前發生的(與雙重檢查自旋鎖相反):
public void doSomething(Runnable r) {
while (!atomicBoolean.compareAndSet(false, true)) {}
// Do some heavy stuff ONCE
r.run();
atomicBoolean.set(false);
}
第一個是更好選擇的原因是它正在做這個做的事情,但是以更優化的方式。
作為旁注,在我的項目中,我實際上使用了下面的代碼(類似於@the8472 的答案),當時我認為是安全的,它可能是:
public T get() {
T res = ref.get();
if (res == null) {
res = builder.get();
if (ref.compareAndSet(null, res))
return res;
else
return ref.get();
} else {
return res;
}
}
關於這段代碼的事情是,作為寫循環中的復制,這個生成多個實例,每個競爭線程一個,但只有一個被緩存,第一個,所有其他構造最終都得到 GC。
看看 putIfAbsent 方法,我看到的好處是跳過了 17 行代碼,然后是同步主體:
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
然后同步體本身是另外 34 行:
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
使用 ConcurrentHashMap 的優點是它無疑會起作用。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.