­

java並發包(1)-AtomicReference和AtomicStampedReference

  • 2019 年 10 月 4 日
  • 筆記

AtomicReference原子應用類,可以保證你在修改對象引用時的線程安全性,比較時可以按照偏移量進行

這裡的cas操作本身是原子的,但是在某些場景下會出現異常場景

線程判斷被修改對象是否可以正確寫入的條件是對象的當前值和期望是否一致。這個邏輯從一般意義上來說是正確的。但有可能出現一個小小的例外,就是當你獲得對象當前數據後,在準備修改為新值前,對象的值被其他線程連續修改了2次,而經過這2次修改後,對象的值又恢復為舊值。這樣,當前線程就無法正確判斷這個對象究竟是否被修改過。如圖所示,顯示了這種情況。(圖片是轉載而來)

一般來說,發生這種情況的概率很小。而且即使發生了,可能也不是什麼大問題。比如,我們只是簡單得要做一個數值加法,即使在我取得期望值後,這個數字被不斷的修改,只要它最終改回了我的期望值,我的加法計算就不會出錯。也就是說,當你修改的對象沒有過程的狀態信息,所有的信息都只保存於對象的數值本身。

但是,在現實中,還可能存在另外一種場景。就是我們是否能修改對象的值,不僅取決於當前值,還和對象的過程變化有關,這時,AtomicReference就無能為力了。

打一個比方,如果有一家蛋糕店,為了挽留客戶,絕對為貴賓卡里餘額小於20元的客戶一次性贈送20元,刺激消費者充值和消費。但條件是,每一位客戶只能被贈送一次。

現在,我們就來模擬這個場景,為了演示AtomicReference,我在這裡使用AtomicReference實現這個功能。

package algorithmProject.concurrent;    import java.util.concurrent.atomic.AtomicReference;    /**   * Created by wangkai on 2017/4/24.   */  public class AtomicReferenceDemo {      // 設置賬戶初始值小於20,顯然這是一個需要被充值的賬戶      static AtomicReference<Integer> money = new AtomicReference<Integer>(19);        public static void main(String args[]) {          //模擬多個線程同時更新後台數據庫,為用戶充值          for (int i = 0; i < 3; i++) {              new Thread() {                  public void run() {                      while (true) {                          while (true) {                              Integer m = money.get();                              if (m < 20) {                                  if (money.compareAndSet(m, m + 20)) {                                      System.out.println("餘額小於20元,充值成功,餘額:" + money.get() + "元");                                      break;                                  }                              } else {                                  //System.out.println("餘額大於20元,無需充值");                                  break;                              }                          }                      }                  }              }.start();          }            //有一個線程一直在消費          new Thread() {              public void run() {                  for (int i = 0; i < 100; i++) {                      while (true) {                          Integer m = money.get();                          if (m > 10) {                              System.out.println("大於10元");                              if (money.compareAndSet(m, m - 10)) {                                  System.out.println("成功消費10元,餘額:" + money.get());                                  break;                              }                          } else {                              System.out.println("沒有足夠的金額");                              break;                          }                      }                      try {                          Thread.sleep(100);                      } catch (InterruptedException e) {                      }                  }              }          }.start();      }  }

首先判斷用戶餘額並給予贈予金額。如果已經被其他用戶處理,那麼當前線程就會失敗。因此,可以確保用戶只會被充值一次。

此時,如果很不幸的,用戶正好正在進行消費,就在贈予金額到賬的同時,他進行了一次消費,使得總金額又小於20元,並且正好累計消費了20元。使得消費、贈予後的金額等於消費前、贈予前的金額。這時,後台的贈予進程就會誤以為這個賬戶還沒有贈予,所以,存在被多次贈予的可能。萬幸的是jdk給我提供了一個類AtomicStampedReference

AtomicReference無法解決上述問題的根本是因為對象在修改過程中,丟失了狀態信息。對象值本身與狀態被畫上了等號。因此,我們只要能夠記錄對象在修改過程中的狀態值,就可以很好的解決對象被反覆修改導致線程無法正確判斷對象狀態的問題。

AtomicStampedReference正是這麼做的。它內部不僅維護了對象值,還維護了一個時間戳(我這裡把它稱為時間戳,實際上它可以使任何一個整數,它使用整數來表示狀態值)。當AtomicStampedReference對應的數值被修改時,除了更新數據本身外,還必須要更新時間戳。當AtomicStampedReference設置對象值時,對象值以及時間戳都必須滿足期望值,寫入才會成功。因此,即使對象值被反覆讀寫,寫回原值,只要時間戳發生變化,就能防止不恰當的寫入。

package algorithmProject.concurrent;    import java.util.concurrent.atomic.AtomicStampedReference;    /**   * Created by wangkai on 2017/4/24.   */  public class AtomicStampedReferenceDemo {      static AtomicStampedReference<Integer> money = new AtomicStampedReference<Integer>(19, 0);        public static void main(String[] args) {          //模擬多個線程同時更新後台數據庫,為用戶充值          for (int i = 0; i < 3; i++) {              //獲得當前時間戳              final int timestamp = money.getStamp();              new Thread() {                  public void run () {                      while (true) {                          while (true) {                              //獲得當前對象引用                              Integer m = money.getReference();                              if (m < 20) {                                  //比較設置 參數依次為:期望值 寫入新值 期望時間戳 新時間戳                                  if (money.compareAndSet(m, m + 20, timestamp, timestamp + 1)) {                                      System.out.println("餘額小於20元,充值成功,餘額:" + money.getReference() + "元");                                      break;                                  }                              } else {                                  //System.out.println("餘額大於20元,無需充值");                                  break;                              }                          }                      }                  }              }.start();          }            //用戶消費線程,模擬消費行為          new Thread() {              public void run() {                  for (int i = 0; i < 100; i++) {                      while (true) {                          int timestamp = money.getStamp();                          Integer m = money.getReference();                          if (m > 10) {                              System.out.println("大於10元");                              if (money.compareAndSet(m, m - 10, timestamp, timestamp + 1)) {                                  System.out.println("成功消費10元,餘額:" + money.getReference());                                  break;                              }                          } else {                              System.out.println("沒有足夠的金額");                              break;                          }                      }                      try {                          Thread.sleep(100);                      } catch (InterruptedException e) {                      }                  }              }          }.start();      }  }