摘要:實戰高并發程序設計連載中的指針類和非常類似,不同之處就在于是對整數的封裝,而則對應普通的對象引用。這樣,當前線程就無法正確判斷這個對象究竟是否被修改過。摘自實戰高并發程序設計一書
【實戰Java高并發程序設計】連載1–Java中的指針:Unsafe類
AtomicReference和AtomicInteger非常類似,不同之處就在于AtomicInteger是對整數的封裝,而AtomicReference則對應普通的對象引用。也就是它可以保證你在修改對象引用時的線程安全性。在介紹AtomicReference的同時,我希望同時提出一個有關原子操作的邏輯上的不足。
之前我們說過,線程判斷被修改對象是否可以正確寫入的條件是對象的當前值和期望是否一致。這個邏輯從一般意義上來說是正確的。但有可能出現一個小小的例外,就是當你獲得對象當前數據后,在準備修改為新值前,對象的值被其他線程連續修改了2次,而經過這2次修改后,對象的值又恢復為舊值。這樣,當前線程就無法正確判斷這個對象究竟是否被修改過。如圖4.2所示,顯示了這種情況。
圖4.2 對象值被反復修改回原數據
一般來說,發生這種情況的概率很小。而且即使發生了,可能也不是什么大問題。比如,我們只是簡單得要做一個數值加法,即使在我取得期望值后,這個數字被不斷的修改,只要它最終改回了我的期望值,我的加法計算就不會出錯。也就是說,當你修改的對象沒有過程的狀態信息,所有的信息都只保存于對象的數值本身。
但是,在現實中,還可能存在另外一種場景。就是我們是否能修改對象的值,不僅取決于當前值,還和對象的過程變化有關,這時,AtomicReference就無能為力了。
打一個比方,如果有一家蛋糕店,為了挽留客戶,絕對為貴賓卡里余額小于20元的客戶一次性贈送20元,刺激消費者充值和消費。但條件是,每一位客戶只能被贈送一次。
現在,我們就來模擬這個場景,為了演示AtomicReference,我在這里使用AtomicReference實現這個功能。首先,我們模擬用戶賬戶余額。
定義用戶賬戶余額:
static AtomicReferencemoney=newAtomicReference (); // 設置賬戶初始值小于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就可以很好的解決這個問題。
摘自《實戰Java高并發程序設計》一書
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/65492.html
摘要:在本例中,講述的無鎖來自于并發包我們將這個無鎖的稱為。在這里,我們使用二維數組來表示的內部存儲,如下變量存放所有的內部元素。為什么使用二維數組去實現一個一維的呢這是為了將來進行動態擴展時可以更加方便。 我們已經比較完整得介紹了有關無鎖的概念和使用方法。相對于有鎖的方法,使用無鎖的方式編程更加考驗一個程序員的耐心和智力。但是,無鎖帶來的好處也是顯而易見的,第一,在高并發的情況下,它比有鎖...
摘要:有時候,由于初期考慮不周,或者后期的需求變化,一些普通變量可能也會有線程安全的需求。它可以讓你在不改動或者極少改動原有代碼的基礎上,讓普通的變量也享受操作帶來的線程安全性,這樣你可以修改極少的代碼,來獲得線程安全的保證。 有時候,由于初期考慮不周,或者后期的需求變化,一些普通變量可能也會有線程安全的需求。如果改動不大,我們可以簡單地修改程序中每一個使用或者讀取這個變量的地方。但顯然,這...
摘要:公平鎖非公平鎖公平鎖公平鎖是指多個線程按照申請鎖的順序來獲取鎖。加鎖后,任何其他試圖再次加鎖的線程會被阻塞,直到當前進程解鎖。重量級鎖會讓其他申請的線程進入阻塞,性能降低。 Java 中15種鎖的介紹 在讀很多并發文章中,會提及各種各樣鎖如公平鎖,樂觀鎖等等,這篇文章介紹各種鎖的分類。介紹的內容如下: 公平鎖 / 非公平鎖 可重入鎖 / 不可重入鎖 獨享鎖 / 共享鎖 互斥鎖 / 讀...
摘要:但是,有些操作會依賴于對象的變化過程,此時的解決思路一般就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那么就會變成。四的引入就是上面所說的加了版本號的。 showImg(https://segmentfault.com/img/remote/1460000016012188); 本文首發于一世流云的專欄:https://segmentfault.com/blo...
摘要:并發包將這種無鎖方案封裝提煉之后,實現了一系列的原子類。無鎖方案相對互斥鎖方案,最大的好處就是性能。作為一條指令,指令本身是能夠保證原子性的。 前面我們多次提到一個累加器的例子,示例代碼如下。在這個例子中,add10K() 這個方法不是線程安全的,問題就出在變量 count 的可見性和 count+=1 的原子性上。可見性問題可以用 volatile 來解決,而原子性問題我們前面一直都...
閱讀 954·2019-08-30 15:55
閱讀 551·2019-08-26 13:56
閱讀 2080·2019-08-26 12:23
閱讀 3295·2019-08-26 10:29
閱讀 600·2019-08-26 10:17
閱讀 2868·2019-08-23 16:53
閱讀 697·2019-08-23 15:55
閱讀 2814·2019-08-23 14:25