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

【實戰Java高併發程序設計】連載1–Java中的指針:Unsafe類數據庫

AtomicReference和AtomicInteger很是相似,不一樣之處就在於AtomicInteger是對整數的封裝,而AtomicReference則對應普通的對象引用。也就是它能夠保證你在修改對象引用時的線程安全性。在介紹AtomicReference的同時,我但願同時提出一個有關原子操做的邏輯上的不足。segmentfault

以前咱們說過,線程判斷被修改對象是否能夠正確寫入的條件是對象的當前值和指望是否一致。這個邏輯從通常意義上來講是正確的。但有可能出現一個小小的例外,就是當你得到對象當前數據後,在準備修改成新值前,對象的值被其餘線程連續修改了2次,而通過這2次修改後,對象的值又恢復爲舊值。這樣,當前線程就沒法正確判斷這個對象到底是否被修改過。如圖4.2所示,顯示了這種狀況。安全

Center
圖4.2 對象值被反覆修改回原數據併發

通常來講,發生這種狀況的機率很小。並且即便發生了,可能也不是什麼大問題。好比,咱們只是簡單得要作一個數值加法,即便在我取得指望值後,這個數字被不斷的修改,只要它最終改回了個人指望值,個人加法計算就不會出錯。也就是說,當你修改的對象沒有過程的狀態信息,全部的信息都只保存於對象的數值自己。高併發

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

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

如今,咱們就來模擬這個場景,爲了演示AtomicReference,我在這裏使用AtomicReference實現這個功能。首先,咱們模擬用戶帳戶餘額。設計

定義用戶帳戶餘額:指針

static AtomicReference<Integer> money=newAtomicReference<Integer>();  
// 設置帳戶初始值小於20,顯然這是一個須要被充值的帳戶  
money.set(19);

接着,咱們須要若干個後臺線程,它們不斷掃描數據,併爲知足條件的客戶充值。code

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

摘自《實戰Java高併發程序設計》一書
Center

相關文章
相關標籤/搜索