【實戰Java高並發程序設計 2】無鎖的對象引用:AtomicReference


  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就可以很好的解決這個問題。

 

推薦本書:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM