Java並發編程之CAS第三篇-CAS的缺點及解決辦法
- 2020 年 3 月 27 日
- 筆記
Java並發編程之CAS第三篇-CAS的缺點
通過前兩篇的文章介紹,我們知道了CAS是什麼以及查看源碼了解CAS原理。那麼在多執行緒並發環境中,的缺點是什麼呢?這篇文章我們就來討論討論
本篇是《凱哥(凱哥Java:kagejava)並發編程學習》系列之《CAS系列》教程的第三篇:CAS的缺點有哪些?怎麼解決。
CAS的缺點
一:do while循環時間長的話開銷大
從源碼中(見上圖),我們可以知道do while中的while返回true會一直循環下去(具體分析步驟見上一篇:《Java並發編程之CAS二源碼追根溯源》。凱哥(凱哥Java:kaigejava)就不在這裡贅述了)。如果並發量很多的話,比如:有十萬個執行緒來並發處理,這這種業務下,很多執行緒都會修改共享變數,要保證原子性的話,循環會很長時間,假設每個執行緒為了保證原子性,循環耗時0.001s的話,那麼十萬個執行緒都這麼循環下來,對CPU的消耗還是比較大的。
二:只能保證一個共享變數的原子性
從源碼中,我們知道 Object var1其實就是對象自己。拿上一篇文章舉的例子來說,其實就是atomicInteger自己,也就是共享變數。CAS的do while只能一個this一個this的比較。從這裡就可以看出,CAS只能保證一個共享變數的原子性。但是如果用同步鎖的話,鎖是可以鎖對象也可以鎖程式碼塊。鎖操作的可以不是一個共享變數。
三:會出現新的問題:ABA問題
何為ABA問題呢?
先來看看現實生活的例子:
學校舉行運動會,標準操場一圈400米,現在正在進行1200米比賽。1200=400*3.需要跑上三圈。小明和小紅比賽,在剛開始的時候,大家都看到小明,小紅都在起點,但是小紅速度比小明快2/3。這個時候,小明爸爸拿著相機拍攝,在起點時候,拍攝小明,3分鐘過後,我們再來看起點,是小紅。7分鐘之後,在看起點是小明。難道小紅就跑了一圈嗎?這當然不對。小紅比小明快,當我們第二次看到小明的時候,小紅其實三圈已經跑完了。最終出現的情況就是:小明(小紅)小紅小明(小紅)小明。最終獲勝的當然是小紅
這個例子或者不是很恰當。但是凱哥是想通過這個例子告訴大家,當執行緒如果出現這種情況的話,會影響到數據結果的。
如下圖:
說明:
A執行緒執行一次耗時:1分鐘
B執行緒執行一次耗時“29.5s
B執行緒在A執行緒執行一次的時間內操作主記憶體的數據變化為:202020192020
當B執行緒執行2次操作之後,1分鐘到了。A執行緒拿著自己工作區copay的副本值i=2020和主記憶體的值i=2020。正好相等,這個時候會,主記憶體的共享變數相對於A執行緒來說,是沒有變化的。但是實際上是有變化的(B執行緒確實操作過的。如上面舉例的,小紅已經跑完三圈了。可是小明才跑第二圈呢),如果這個時候在操作,有可能導致數據出問題(賽跑最終結果是小紅贏了,而不是小明贏了)。
所謂的ABA就是:在某個監控點的時候數據是A,當過了時間N之後,在監控的時候還是A。但是在時間N的這段時間內,監控點的數據有可能不是A了,變成過B。這樣就更容易理解了吧。
ABA問題演示程式碼:
程式碼說明:
初始的時候,給了變數值為2020.也就是V=2020.如上圖1
在經過執行緒A一頓猛如虎的操作之後,搞出來2020,2021,2020.ABA的效果處理。如上圖2.
Sleep了1秒是為了讓執行緒A完成ABA操作的。
然後,執行緒2在拿著自己副本的變數值A=2020,和主記憶體V進行比較。發現一致,就更新了2019.
運行結果如下圖:
從運行結果來看,執行緒2也更新成功了。但是,這樣是不對的。因為我們已經知道執行緒A對共享變數操作過了。那麼針對CAS的這些缺點,應該怎麼解決呢?歡迎繼續學習下一篇。凱哥將介紹三個怎麼解決。以及會講解原子引用、時間戳原子引用兩個問題。
CAS缺點解決辦法
一:循環長,開銷大解決方案
解決思路:ConcurrentHashMap(後面凱哥也會詳細介紹的)類似的方法。當多個執行緒競爭時,將粒度變小,將一個變數拆分為多個變數,達到多個執行緒訪問多個資源的效果,最後再調用sum把它合起來。
二:一個共享變數的解決方案
因為CAS只能一個共享變數一個共享變數的處理。如果想要處理類是程式碼塊或者對象的。可以使用同步鎖或者是多個變數放到一個對象裡面。然後在CAS。因為在JUC包下,有支援對象的原子類,如:AtomicReference(原子引用類)。
原子引用
在Java中變數的類型分為八大基本類型或八大基本類型的對象類型或者是自定義的對象類型。在並發中,atomicInteger就是基本類型就是int/Integer的原子類。那麼自定義的對象怎麼實現原子性呢?這就要用到原子引用對象- AtomicReference。
原子引用demo:
我們來模擬凱哥心中女神變化過程(註:女神同時只能存在一個,不能存在多個,要保持單一,原子的)。
在X年之前是劉亦菲,X+N年後是林依晨,現在是佟麗婭了。我們知道,這三個女神都是對象。都有年齡、用戶名,是個對象。
創建user對象
她們三個在凱哥心中活動如下:
那麼請問在21和23行輸入的結果是什麼?
編輯
我們發現在23行依然輸出的是林依晨。而不是佟麗婭。為什麼呢?分析思路見:《Java並發編程之CAS一理解》篇文章的三:cas程式碼演示部分。
我們修改之後再來看:
運行結果:
發現心中女神已經更新為佟麗婭了
三:ABA問題解決
ABA問題產生的根本原因是因為:只是執行緒自己工作空間的變數預期值(副本)和主記憶體中的值進行了比較。當值相等的時候,就默認沒有被其他執行緒更新過。那麼怎麼解決這個問題呢?
是不是可以添加一個東西,用來輔助呢?添加一個標記,或者一個版本號,根據版本號+數值來進行判斷呢?當然可以了,JDK中也是這麼實現的。JDK使用的是時間戳(stamp),而不是我們說的版本號(version)。我們來看看時間戳原子引用(AtomicStampedReference<V>).
我們來看看這個類。
時間戳原子引用demo
先看構造器:
參數說明:
initialRef:初始值
initialStamp:初始值的時間戳
再來看看CompareAndSet方法:
參數說明:
expectedReference:預期值
newReference:更新值
expectedStamp:預期時間戳值
newStamp:更改後時間戳值
我們發現這個AtomicStampedReference類和AtomicReference的方法中的區別就是時間戳原子引用類中的方法都添加了預期的時間戳值和修改後的時間戳的值這兩個參數。
我們來看看,使用帶有時間戳的原子引用類解決ABA問題的程式碼:
1:聲明共享變數
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(127,1);
(需要說明,如果用數值做demo的話,主要int的取值範圍。如果大於127,就會始終返回false。因為 Integer(128) == Integer(128)返回的是false)
執行緒一先修改執行一個ABA的過程:
編輯
執行完成之後,當前的主記憶體中版本號應該是3了。
我們在用執行緒2來執行compareAndSet:
此時,在執行緒2中的版本號:tamp應該是1,但是主記憶體中的版本號已經是3了。所以執行後返回false.執行不成功的。
我們來看看運行結果和我們預期結果:
運行結果,和我們預期結果是一致的。說明,添加這個時間戳(版本號)可以解決ABA問題