簡體   English   中英

原子 compareAndSet 但有回調?

[英]Atomic compareAndSet but with callback?

我知道AtomicReferencecompareAndSet ,但我覺得我想做的是這個

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 更新方法應該是無副作用的。

否則,您應該只使用帶有變量引用字段的雙重檢查鎖定模式。

當然,您始終可以將這些中的任何一個打包到一個高階函數中,該函數返回一個RunnableSupplier ,然后您將其分配給最終字段。

// ==  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 ,但這允許“東西”的多個並發執行。 這也是updateAndGetaccumulateAndGet發生的情況,它提供了函數。

為了保證在繼續之前只執行一次,如果“東西”當前正在執行,線程必須被阻塞。 上面的代碼就是這樣做的。 請注意,一旦“東西”完成,將不再有鎖定,並且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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM