Java併發編程之CAS第三篇-CAS的缺點java
經過前兩篇的文章介紹,咱們知道了CAS是什麼以及查看源碼瞭解CAS原理。那麼在多線程併發環境中,的缺點是什麼呢?這篇文章咱們就來討論討論編程
本篇是《凱哥(凱哥Java:kagejava)併發編程學習》系列之《CAS系列》教程的第三篇:CAS的缺點有哪些?怎麼解決。多線程
從源碼中(見上圖),咱們能夠知道do while中的while返回true會一直循環下去(具體分析步驟見上一篇:《Java併發編程之CAS二源碼追根溯源》。凱哥(凱哥Java:kaigejava)就不在這裏贅述了)。若是併發量不少的話,好比:有十萬個線程來併發處理,這這種業務下,不少線程都會修改共享變量,要保證原子性的話,循環會很長時間,假設每一個線程爲了保證原子性,循環耗時0.001s的話,那麼十萬個線程都這麼循環下來,對CPU的消耗仍是比較大的。併發
從源碼中,咱們知道 Object var1其實就是對象本身。拿上一篇文章舉的例子來講,其實就是atomicInteger本身,也就是共享變量。CAS的do while只能一個this一個this的比較。從這裏就能夠看出,CAS只能保證一個共享變量的原子性。可是若是用同步鎖的話,鎖是能夠鎖對象也能夠鎖代碼塊。鎖操做的能夠不是一個共享變量。ide
先來看看現實生活的例子:學習
學校舉行運動會,標準操場一圈400米,如今正在進行1200米比賽。1200=400*3.須要跑上三圈。小明和小紅比賽,在剛開始的時候,你們都看到小明,小紅都在起點,可是小紅速度比小明快2/3。這個時候,小明爸爸拿着相機拍攝,在起點時候,拍攝小明,3分鐘事後,咱們再來看起點,是小紅。7分鐘以後,在看起點是小明。難道小紅就跑了一圈嗎?這固然不對。小紅比小明快,當咱們第二次看到小明的時候,小紅其實三圈已經跑完了。最終出現的狀況就是:小明(小紅)小紅小明(小紅)小明。最終獲勝的固然是小紅this
這個例子或者不是很恰當。可是凱哥是想經過這個例子告訴你們,當線程若是出現這種狀況的話,會影響到數據結果的。atom
以下圖:spa
說明:線程
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。這樣就更容易理解了吧。
代碼說明:
初始的時候,給了變量值爲2020.也就是V=2020.如上圖1
在通過線程A一頓猛如虎的操做以後,搞出來2020,2021,2020.ABA的效果處理。如上圖2.
Sleep了1秒是爲了讓線程A完成ABA操做的。
而後,線程2在拿着本身副本的變量值A=2020,和主內存V進行比較。發現一致,就更新了2019.
運行結果以下圖:
從運行結果來看,線程2也更新成功了。可是,這樣是不對的。由於咱們已經知道線程A對共享變量操做過了。那麼針對CAS的這些缺點,應該怎麼解決呢?歡迎繼續學習下一篇。凱哥將介紹三個怎麼解決。以及會講解原子引用、時間戳原子引用兩個問題。
解決思路:ConcurrentHashMap(後面凱哥也會詳細介紹的)相似的方法。當多個線程競爭時,將粒度變小,將一個變量拆分爲多個變量,達到多個線程訪問多個資源的效果,最後再調用sum把它合起來。
由於CAS只能一個共享變量一個共享變量的處理。若是想要處理類是代碼塊或者對象的。可使用同步鎖或者是多個變量放到一個對象裏面。而後在CAS。由於在JUC包下,有支持對象的原子類,如:AtomicReference(原子引用類)。
在Java中變量的類型分爲八大基本類型或八大基本類型的對象類型或者是自定義的對象類型。在併發中,atomicInteger就是基本類型就是int/Integer的原子類。那麼自定義的對象怎麼實現原子性呢?這就要用到原子引用對象- AtomicReference。
咱們來模擬凱哥心中女神變化過程(注:女神同時只能存在一個,不能存在多個,要保持單一,原子的)。
在X年以前是劉亦菲,X+N年後是林依晨,如今是佟麗婭了。咱們知道,這三個女神都是對象。都有年齡、用戶名,是個對象。
建立user對象
她們三個在凱哥心中活動以下:
那麼請問在21和23行輸入的結果是什麼?
編輯
咱們發如今23行依然輸出的是林依晨。而不是佟麗婭。爲何呢?分析思路見:《Java併發編程之CAS一理解》篇文章的三:cas代碼演示部分。
咱們修改以後再來看:
運行結果:
發現心中女神已經更新爲佟麗婭了
ABA問題產生的根本緣由是由於:只是線程本身工做空間的變量預期值(副本)和主內存中的值進行了比較。當值相等的時候,就默認沒有被其餘線程更新過。那麼怎麼解決這個問題呢?
是否是能夠添加一個東西,用來輔助呢?添加一個標記,或者一個版本號,根據版本號+數值來進行判斷呢?固然能夠了,JDK中也是這麼實現的。JDK使用的是時間戳(stamp),而不是咱們說的版本號(version)。咱們來看看時間戳原子引用(AtomicStampedReference<V>).
咱們來看看這個類。
先看構造器:
參數說明:
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問題