1. 程式人生 > >十分鐘解決Java併發面試問題:Java 8如何優化CAS效能?

十分鐘解決Java併發面試問題:Java 8如何優化CAS效能?

一、場景引入,問題凸現
篇幅較長就開門見山吧!假設多個執行緒需要對一個變數不停的累加1,比如說下面這段程式碼:
在這裡插入圖片描述
實際上,上面那段程式碼是不ok的,因為多個執行緒直接這樣併發的對一個data變數進行修改,是執行緒不安全性的行為,會導致data值的變化不遵照預期的值來改變。

舉個例子,比如說20個執行緒分別對data執行一次data++操作,我們以為最後data的值會變成20,其實不是。

最後可能data的值是18,或者是19,都有可能,因為多執行緒併發操作下,就是會有這種安全問題,導致資料結果不準確。

至於為什麼會不準確?那不在本文討論的範圍裡,因為這個一般只要是學過java的同學,肯定都瞭解過多執行緒併發問題。

二、初步的解決方案:synchronized
所以,對於上面的程式碼,一般我們會改造一下,讓他通過加鎖的方式變成執行緒安全的:
在這裡插入圖片描述
這個時候,程式碼就是執行緒安全的了,因為我們加了synchronized,也就是讓每個執行緒要進入increment()方法之前先得嘗試加鎖,同一時間只有一個執行緒能加鎖,其他執行緒需要等待鎖。

通過這樣處理,就可以保證換個data每次都會累加1,不會出現資料錯亂的問題。

但是,如此簡單的data++操作,都要加一個重磅的synchronized鎖來解決多執行緒併發問題,就有點殺雞用牛刀,大材小用了。

雖然隨著Java版本更新,也對synchronized做了很多優化,但是處理這種簡單的累加操作,仍然顯得“太重了”。人家synchronized是可以解決更加複雜的併發程式設計場景和問題的。

而且,在這個場景下,你要是用synchronized,不就相當於讓各個執行緒序列化了麼?一個接一個的排隊,加鎖,處理資料,釋放鎖,下一個再進來。

三、更高效的方案:Atomic原子類及其底層原理
對於這種簡單的data++類的操作,其實我們完全可以換一種做法,java併發包下面提供了一系列的Atomic原子類,比如說AtomicInteger。

他可以保證多執行緒併發安全的情況下,高效能的併發更新一個數值。我們來看下面的程式碼:

在這裡插入圖片描述
大家看上面的程式碼,是不是很簡單!多個執行緒可以併發的執行AtomicInteger的incrementAndGet()方法,意思就是給我把data的值累加1,接著返回累加後最新的值。

這個程式碼裡,就沒有看到加鎖和釋放鎖這一說了吧!

實際上,Atomic原子類底層用的不是傳統意義的鎖機制,而是無鎖化的CAS機制,通過CAS機制保證多執行緒修改一個數值的安全性

那什麼是CAS呢?他的全稱是:Compare and Set,也就是先比較再設定的意思。
在這裡插入圖片描述Java企業級電商專案架構演進之路 Tomcat叢集與Redis分散式和Java深入微服務原理改造房產銷售平臺,需要完整Java全套資料可以掃下方微信碼免費領取
在這裡插入圖片描述專案效果展示圖

假如說有3個執行緒併發的要修改一個AtomicInteger的值,他們底層的機制如下:

首先,每個執行緒都會先獲取當前的值,接著走一個原子的CAS操作,原子的意思就是這個CAS操作一定是自己完整執行完的,不會被別人打斷。

然後CAS操作裡,會比較一下說,唉!大兄弟!現在你的值是不是剛才我獲取到的那個值啊?如果是的話,bingo!說明沒人改過這個值,那你給我設定成累加1之後的一個值好了!

同理,如果有人在執行CAS的時候,發現自己之前獲取的值跟當前的值不一樣,會導致CAS失敗,失敗之後,進入一個無限迴圈,再次獲取值,接著執行CAS操作!

四、Java 8對CAS機制的優化

但是這個CAS有沒有問題呢?肯定是有的。比如說大量的執行緒同時併發修改一個AtomicInteger,可能有很多執行緒會不停的自旋,進入一個無限重複的迴圈中。

這些執行緒不停地獲取值,然後發起CAS操作,但是發現這個值被別人改過了,於是再次進入下一個迴圈,獲取值,發起CAS操作又失敗了,再次進入下一個迴圈。

在大量執行緒高併發更新AtomicInteger的時候,這種問題可能會比較明顯,導致大量執行緒空迴圈,自旋轉,效能和效率都不是特別好。

於是,噹噹噹當,Java 8推出了一個新的類,LongAdder,他就是嘗試使用分段CAS以及自動分段遷移的方式來大幅度提升多執行緒高併發執行CAS操作的效能!
在這裡插入圖片描述
在LongAdder的底層實現中,首先有一個base值,剛開始多執行緒來不停的累加數值,都是對base進行累加的,比如剛開始累加成了base = 5。

接著如果發現併發更新的執行緒數量過多,就會開始施行分段CAS的機制,也就是內部會搞一個Cell陣列,每個陣列是一個數值分段。

這時,讓大量的執行緒分別去對不同Cell內部的value值進行CAS累加操作,這樣就把CAS計算壓力分散到了不同的Cell分段數值中了!

這樣就可以大幅度的降低多執行緒併發更新同一個數值時出現的無限迴圈的問題,大幅度提升了多執行緒併發更新數值的效能和效率!

而且他內部實現了自動分段遷移的機制,也就是如果某個Cell的value執行CAS失敗了,那麼就會自動去找另外一個Cell分段內的value值進行CAS操作。

這樣也解決了執行緒空旋轉、自旋不停等待執行CAS操作的問題,讓一個執行緒過來執行CAS時可以儘快的完成這個操作。

最後,如果你要從LongAdder中獲取當前累加的總值,就會把base值和所有Cell分段數值加起來返回給你。

五、總結 & 思考
不知道大家有沒有發現這種高併發訪問下的分段處理機制,在很多地方都有類似的思想體現!因為高併發中的分段處理機制實際上是一個很常見和常用的併發優化手段。

所以其實很多技術,思想都是有異曲同工之妙的。

歡迎掃碼下方二維碼領取資料。之後我會給大家推薦合適的網路課程。
(PS:所擁有資料及所推薦課程皆是免費的)
在這裡插入圖片描述