Java 併發:使用 AtomicReference 處理競態條件

為什麼要用 AtomicReference?


我正在開發一個工具軟件,需要檢測某個對象是否被多個線程調用。為此我用到了以下的不可變類:


public class State {    private final Thread thread;    private final boolean accessedByMultipleThreads;    public State(Thread thread, boolean accessedByMultipleThreads) {        super();        this.thread = thread;        this.accessedByMultipleThreads = accessedByMultipleThreads;    }    public State() {        super();        this.thread = null;        this.accessedByMultipleThreads = false;    }    public State update() {        if(accessedByMultipleThreads)   {            return this;        }        if( thread == null  ) {            return new  State(Thread.currentThread()            , accessedByMultipleThreads);        }        if(thread != Thread.currentThread()) {            return new  State(null,true);        }        return this;    }    public boolean isAccessedByMultipleThreads() {        return accessedByMultipleThreads;    }}


第2行,thread 變量中存放了訪問對象的第一個線程。第23行,當有其它線程訪問該對象時,把 accessedByMultipleThreads 變量設為 true,並把 thread 變量置為 null。第15至17行,當 accessedByMultipleThreads 變量為 true 時,不改變 state。

在每個對象中使用這個類,檢查是否被多個線程訪問。下面的 UpdateStateNotThreadSafe 展示瞭如何使用 state:


public class UpdateStateNotThreadSafe {  private volatile  State state = new State();  public void update() {    state = state.update();  }  public State getState() {    return state;  }}


第2行,把 state 存入 volatile 變量。這裡需要使用 volatile 關鍵字確保線程始終能夠看到當前值。


下面這個測試用來檢查 volatile 變量是否線程安全:


import com.vmlens.api.AllInterleavings;public class TestNotThreadSafe {@Testpublic void test() throws InterruptedException {    try (AllInterleavings allInterleavings =        new AllInterleavings(“TestNotThreadSafe”);){        while (allInterleavings.hasNext()) {    final UpdateStateNotThreadSafe object  = new UpdateStateNotThreadSafe();    Thread first = new Thread(() -> { object.update(); });Thread second = new Thread(() -> { object.update(); });first.start();second.start();first.join();second.join();assertTrue(object.getState().isAccessedByMultipleThreads());    }}}}


第9至10行創建了兩個線程,用來測試在 volatile 變量是否線程安全。第11至12行啟動這兩個線程,直到兩個線程調用線程 join 操作結束(第13至14行)。第15行,當兩個線程停止後,檢查 accessedByMultipleThreads 是否為 true。


為了測試所有線程的交叉情況,第7行的 while 循環會遍歷 AllInterleavings 類中所有 interleavingvmlens。(譯註:interleaving 是 vmlens 開發庫多線程測試中的概念)運行測試看到以下錯誤:


java.lang.AssertionError:    at org.junit.Assert.fail(Assert.java:91)    at org.junit.Assert.assertTrue(Assert.java:43)    at org.junit.Assert.assertTrue(Assert.java:54)


vmlens 的報告揭示了問題所在:

Java 併發:使用 AtomicReference 處理競態條件

這裡的問題在於,對於特定線程兩個線程會首先交叉讀取 state。因此,一個線程會覆蓋另一個線程的結果。


如何使用 AtomicReference?


為了解決這種競態條件,我使用 AtomicReference 的 compareAndSet 方法。


compareAndSet 方法有兩個參數,期望值和更新值。該方法會自動檢測當前值與期望值是否相等。如果相等,會設置為更新值並返回 true。如果不等,則當前值保持不變並返回 false。


這種方法的主要思想,通過 compareAndSet 檢查計算新值的過程中,當前值是否被另一個線程修改。如果沒有,可以安全地更新當前值。否則,需要使用更改後的當前值重新計算新值。


下面顯示瞭如何使用 compareAndSet 方法自動更新 state:


public class UpdateStateWithCompareAndSet {  private final AtomicReference state = new AtomicReference(new State());  public  void update() {    State current = state.get();    State newValue = current.update();    while( ! state.compareAndSet( current , newValue ) ) {      current = state.get();      newValue = current.update();    }  }  public State getState() {    return state.get();  }}


第2行,對 state 變量使用了AtomicReference。第5行,要更新 state 首先需要獲取當前值。然後,在第6行計算新值,並在第7行嘗試使用 compareAndSet 更新 AtomicReference。如果更新成功,則處理完畢。如果沒有成功,需要在第8行再次獲取當前值,並在第9行重新計算新值。然後,再次嘗試使用 compareAndSet 更新 AtomicReference。使用 while 循環是因為 compareAndSet 可能會多次失敗。


總結


使用 volatile 變量會引發競態條件,因為某個特定線程的交叉訪問會覆蓋其它線程的計算結果。通過使用 AtomicReference 類中的 compareAndSet 方法可以規避這種競態條件。通過這種方法,可以自動檢查當前值是否與開始計算時相同。如果相同,可以安全地更新當前值。否則,需要用更改後的當前值重新計算新值。


分享到:


相關文章: