AtomicReference和AtomicInteger非常類似,不同之處就在於AtomicInteger是對整數的封裝,而AtomicReference則對應普通的對象引用。也就是它可以保證你在修改對象引用時的線程安全性。在介紹AtomicReference的同時,我希望同時提出一個有關原子操作的邏輯上的不足。
之前我們說過,線程判斷被修改對象是否可以正確寫入的條件是對象的當前值和期望是否一致。這個邏輯從一般意義上來說是正確的。但有可能出現一個小小的例外,就是當你獲得對象當前數據後,在準備修改爲新值前,對象的值被其他線程連續修改了2次,而經過這2次修改後,對象的值又恢復爲舊值。這樣,當前線程就無法正確判斷這個對象究竟是否被修改過。如圖4.2所示,顯示了這種情況。
圖4.2 對象值被反覆修改回原數據
一般來說,發生這種情況的概率很小。而且即使發生了,可能也不是什麼大問題。比如,我們只是簡單得要做一個數值加法,即使在我取得期望值後,這個數字被不斷的修改,只要它最終改回了我的期望值,我的加法計算就不會出錯。也就是說,當你修改的對象沒有過程的狀態信息,所有的信息都只保存於對象的數值本身。
但是,在現實中,還可能存在另外一種場景。就是我們是否能修改對象的值,不僅取決於當前值,還和對象的過程變化有關,這時,AtomicReference就無能爲力了。
打一個比方,如果有一家蛋糕店,爲了挽留客戶,絕對爲貴賓卡里餘額小於20元的客戶一次性贈送20元,刺激消費者充值和消費。但條件是,每一位客戶只能被贈送一次。
現在,我們就來模擬這個場景,爲了演示AtomicReference,我在這裏使用AtomicReference實現這個功能。首先,我們模擬用戶賬戶餘額。
定義用戶賬戶餘額:
static AtomicReference<Integer> money=newAtomicReference<Integer>(); // 設置賬戶初始值小於20,顯然這是一個需要被充值的賬戶 money.set(19);
接着,我們需要若干個後臺線程,它們不斷掃描數據,併爲滿足條件的客戶充值。
01 //模擬多個線程同時更新後臺數據庫,爲用戶充值 02 for(int i = 0 ; i < 3 ; i++) { 03 new Thread(){ 04 publicvoid run() { 05 while(true){ 06 while(true){ 07 Integer m=money.get(); 08 if(m<20){ 09 if(money.compareAndSet(m, m+20)){ 10 System.out.println("餘額小於20元,充值成功,餘額:"+money.get()+"元"); 11 break; 12 } 13 }else{ 14 //System.out.println("餘額大於20元,無需充值"); 15 break ; 16 } 17 } 18 } 19 } 20 }.start(); 21 }
上述代碼第8行,判斷用戶餘額並給予贈予金額。如果已經被其他用戶處理,那麼當前線程就會失敗。因此,可以確保用戶只會被充值一次。
此時,如果很不幸的,用戶正好正在進行消費,就在贈予金額到賬的同時,他進行了一次消費,使得總金額又小於20元,並且正好累計消費了20元。使得消費、贈予後的金額等於消費前、贈予前的金額。這時,後臺的贈予進程就會誤以爲這個賬戶還沒有贈予,所以,存在被多次贈予的可能。下面,模擬了這個消費線程:
01 //用戶消費線程,模擬消費行爲 02 new Thread() { 03 public voidrun() { 04 for(inti=0;i<100;i++){ 05 while(true){ 06 Integer m=money.get(); 07 if(m>10){ 08 System.out.println("大於10元"); 09 if(money.compareAndSet(m, m-10)){ 10 System.out.println("成功消費10元,餘額:"+money.get()); 11 break; 12 } 13 }else{ 14 System.out.println("沒有足夠的金額"); 15 break; 16 } 17 } 18 try{Thread.sleep(100);} catch (InterruptedException e) {} 19 } 20 } 21 }.start();
上述代碼中,消費者只要貴賓卡里的錢大於10元,就會立即進行一次10元的消費。執行上述程序,得到的輸出如下:
餘額小於20元,充值成功,餘額:39元 大於10元 成功消費10元,餘額:29 大於10元 成功消費10元,餘額:19 餘額小於20元,充值成功,餘額:39元 大於10元 成功消費10元,餘額:29 大於10元 成功消費10元,餘額:39 餘額小於20元,充值成功,餘額:39元
從這一段輸出中,可以看到,這個賬戶被先後反覆多次充值。其原因正是因爲賬戶餘額被反覆修改,修改後的值等於原有的數值。使得CAS操作無法正確判斷當前數據狀態。
雖然說這種情況出現的概率不大,但是依然是有可能的出現的。因此,當業務上確實可能出現這種情況時,我們也必須多加防範。體貼的JDK也已經爲我們考慮到了這種情況,使用AtomicStampedReference就可以很好的解決這個問題。
推薦本書: