Java併發編程之CAS三CAS的缺點 及解決辦法

Java併發編程之CAS第三篇-CAS的缺點

通過前兩篇的文章介紹,我們知道了CAS是什麼以及查看源碼瞭解CAS原理。那麼在多線程併發環境中,的缺點是什麼呢?這篇文章我們就來討論討論

本篇是《凱哥(凱哥Java:kagejava)併發編程學習》系列之《CAS系列》教程的第三篇:CAS的缺點有哪些?怎麼解決。

Java併發編程之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分鐘之後,在看起點是小明。難道小紅就跑了一圈嗎?這當然不對。小紅比小明快,當我們第二次看到小明的時候,小紅其實三圈已經跑完了。最終出現的情況就是:小明(小紅)小紅小明(小紅)小明。最終獲勝的當然是小紅

這個例子或者不是很恰當。但是凱哥是想通過這個例子告訴大家,當線程如果出現這種情況的話,會影響到數據結果的。

如下圖:

Java併發編程之CAS三CAS的缺點 及解決辦法


說明:

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問題演示代碼:

Java併發編程之CAS三CAS的缺點 及解決辦法


代碼說明:

初始的時候,給了變量值為2020.也就是V=2020.如上圖1

在經過線程A一頓猛如虎的操作之後,搞出來2020,2021,2020.ABA的效果處理。如上圖2.

Sleep了1秒是為了讓線程A完成ABA操作的。

然後,線程2在拿著自己副本的變量值A=2020,和主內存V進行比較。發現一致,就更新了2019.

運行結果如下圖:

Java併發編程之CAS三CAS的缺點 及解決辦法


從運行結果來看,線程2也更新成功了。但是,這樣是不對的。因為我們已經知道線程A對共享變量操作過了。那麼針對CAS的這些缺點,應該怎麼解決呢?歡迎繼續學習下一篇。凱哥將介紹三個怎麼解決。以及會講解原子引用、時間戳原子引用兩個問題。

CAS缺點解決辦法

一:循環長,開銷大解決方案

解決思路:ConcurrentHashMap(後面凱哥也會詳細介紹的)類似的方法。當多個線程競爭時,將粒度變小,將一個變量拆分為多個變量,達到多個線程訪問多個資源的效果,最後再調用sum把它合起來。

二:一個共享變量的解決方案

因為CAS只能一個共享變量一個共享變量的處理。如果想要處理類是代碼塊或者對象的。可以使用同步鎖或者是多個變量放到一個對象裡面。然後在CAS。因為在JUC包下,有支持對象的原子類,如:AtomicReference(原子引用類)。

原子引用

在Java中變量的類型分為八大基本類型或八大基本類型的對象類型或者是自定義的對象類型。在併發中,atomicInteger就是基本類型就是int/Integer的原子類。那麼自定義的對象怎麼實現原子性呢?這就要用到原子引用對象- AtomicReference。

原子引用demo:

我們來模擬凱哥心中女神變化過程(注:女神同時只能存在一個,不能存在多個,要保持單一,原子的)。

在X年之前是劉亦菲,X+N年後是林依晨,現在是佟麗婭了。我們知道,這三個女神都是對象。都有年齡、用戶名,是個對象。

創建user對象

Java併發編程之CAS三CAS的缺點 及解決辦法


她們三個在凱哥心中活動如下:

Java併發編程之CAS三CAS的缺點 及解決辦法


那麼請問在21和23行輸入的結果是什麼?

Java併發編程之CAS三CAS的缺點 及解決辦法

我們發現在23行依然輸出的是林依晨。而不是佟麗婭。為什麼呢?分析思路見:《Java併發編程之CAS一理解》篇文章的三:cas代碼演示部分。

我們修改之後再來看:

Java併發編程之CAS三CAS的缺點 及解決辦法


運行結果:

Java併發編程之CAS三CAS的缺點 及解決辦法


發現心中女神已經更新為佟麗婭了

三:ABA問題解決

ABA問題產生的根本原因是因為:只是線程自己工作空間的變量預期值(副本)和主內存中的值進行了比較。當值相等的時候,就默認沒有被其他線程更新過。那麼怎麼解決這個問題呢?

是不是可以添加一個東西,用來輔助呢?添加一個標記,或者一個版本號,根據版本號+數值來進行判斷呢?當然可以了,JDK中也是這麼實現的。JDK使用的是時間戳(stamp),而不是我們說的版本號(version)。我們來看看時間戳原子引用(AtomicStampedReference).

我們來看看這個類。

時間戳原子引用demo

先看構造器:

Java併發編程之CAS三CAS的缺點 及解決辦法


參數說明:

initialRef:初始值

initialStamp:初始值的時間戳

再來看看CompareAndSet方法:

Java併發編程之CAS三CAS的缺點 及解決辦法


參數說明:

expectedReference:預期值

newReference:更新值

expectedStamp:預期時間戳值

newStamp:更改後時間戳值

我們發現這個AtomicStampedReference類和AtomicReference的方法中的區別就是時間戳原子引用類中的方法都添加了預期的時間戳值和修改後的時間戳的值這兩個參數。

我們來看看,使用帶有時間戳的原子引用類解決ABA問題的代碼:

static AtomicStampedReference<integer> atomicStampedReference = new AtomicStampedReference<>(127,1);/<integer>

(需要說明,如果用數值做demo的話,主要int的取值範圍。如果大於127,就會始終返回false。因為 Integer(128) == Integer(128)返回的是false)

線程一先修改執行一個ABA的過程:

Java併發編程之CAS三CAS的缺點 及解決辦法

執行完成之後,當前的主內存中版本號應該是3了。

我們在用線程2來執行compareAndSet:

Java併發編程之CAS三CAS的缺點 及解決辦法


此時,在線程2中的版本號:tamp應該是1,但是主內存中的版本號已經是3了。所以執行後返回false.執行不成功的。

我們來看看運行結果和我們預期結果:

Java併發編程之CAS三CAS的缺點 及解決辦法


運行結果,和我們預期結果是一致的。說明,添加這個時間戳(版本號)可以解決ABA問題


分享到:


相關文章: