歡迎關注我的公衆號:石杉的架構筆記(ID:shishan100)
java
週一至週五早8點半!精品技術文章準時送上!面試
上篇文章給你們聊了一下volatile的原理,具體參見:大白話聊聊Java併發面試問題之volatile究竟是什麼?。算法
這篇文章給你們聊一下java併發包下的CAS相關的原子操做,以及Java 8如何改進和優化CAS操做的性能。編程
由於Atomic系列的原子類,不管在併發編程、JDK源碼、仍是各類開源項目中,都常常用到。並且在Java併發面試中,這一塊也屬於比較高頻的考點,因此仍是值得給你們聊一聊。數組
好,咱們正式開始!假設多個線程須要對一個變量不停的累加1,好比說下面這段代碼:安全
實際上,上面那段代碼是不ok的,由於多個線程直接這樣併發的對一個data變量進行修改,是線程不安全性的行爲,會致使data值的變化不遵守預期的值來改變。性能優化
舉個例子,好比說20個線程分別對data執行一次data++操做,咱們覺得最後data的值會變成20,其實不是。多線程
最後可能data的值是18,或者是19,都有可能,由於多線程併發操做下,就是會有這種安全問題,致使數據結果不許確。架構
至於爲何會不許確?那不在本文討論的範圍裏,由於這個通常只要是學過java的同窗,確定都瞭解過多線程併發問題。併發
因此,對於上面的代碼,通常咱們會改造一下,讓他經過加鎖的方式變成線程安全的:
這個時候,代碼就是線程安全的了,由於咱們加了synchronized,也就是讓每一個線程要進入increment()方法以前先得嘗試加鎖,同一時間只有一個線程能加鎖,其餘線程須要等待鎖。
經過這樣處理,就能夠保證換個data每次都會累加1,不會出現數據錯亂的問題。
老規矩!咱們來看看下面的圖,感覺一下synchronized加鎖下的效果和氛圍,至關於N個線程一個一個的排隊在更新那個數值。
可是,如此簡單的data++操做,都要加一個重磅的synchronized鎖來解決多線程併發問題,就有點殺雞用牛刀,大材小用了。
雖然隨着Java版本更新,也對synchronized作了不少優化,可是處理這種簡單的累加操做,仍然顯得「過重了」。人家synchronized是能夠解決更加複雜的併發編程場景和問題的。
並且,在這個場景下,你要是用synchronized,不就至關於讓各個線程串行化了麼?一個接一個的排隊,加鎖,處理數據,釋放鎖,下一個再進來。
對於這種簡單的data++類的操做,其實咱們徹底能夠換一種作法,java併發包下面提供了一系列的Atomic原子類,好比說AtomicInteger。
他能夠保證多線程併發安全的狀況下,高性能的併發更新一個數值。咱們來看下面的代碼:
你們看上面的代碼,是否是很簡單!多個線程能夠併發的執行AtomicInteger的incrementAndGet()方法,意思就是給我把data的值累加1,接着返回累加後最新的值。
這個代碼裏,就沒有看到加鎖和釋放鎖這一說了吧!
實際上,Atomic原子類底層用的不是傳統意義的鎖機制,而是無鎖化的CAS機制,經過CAS機制保證多線程修改一個數值的安全性
那什麼是CAS呢?他的全稱是:Compare and Set,也就是先比較再設置的意思。
話很少說,先上圖!
咱們來看上面的圖,假如說有3個線程併發的要修改一個AtomicInteger的值,他們底層的機制以下:
首先,每一個線程都會先獲取當前的值,接着走一個原子的CAS操做,原子的意思就是這個CAS操做必定是本身完整執行完的,不會被別人打斷。
而後CAS操做裏,會比較一下說,唉!大兄弟!如今你的值是否是剛纔我獲取到的那個值啊?
若是是的話,bingo!說明沒人改過這個值,那你給我設置成累加1以後的一個值好了!
同理,若是有人在執行CAS的時候,發現本身以前獲取的值跟當前的值不同,會致使CAS失敗,失敗以後,進入一個無限循環,再次獲取值,接着執行CAS操做!
好!如今咱們對照着上面的圖,來看一下這整個過程:
上述整個過程,就是所謂Atomic原子類的原理,沒有基於加鎖機制串行化,而是基於CAS機制:先獲取一個值,而後發起CAS,比較這個值被人改過沒?若是沒有,就更改值!這個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分段數值加起來返回給你。
不知道你們有沒有發現這種高併發訪問下的分段處理機制,在不少地方都有相似的思想體現!由於高併發中的分段處理機制其實是一個很常見和經常使用的併發優化手段。
在咱們以前的一篇講分佈式鎖的文章:(每秒上千訂單場景下的分佈式鎖高併發優化實踐!),也是用到了分段加鎖以及自動分段遷移/合併加鎖的一套機制,來大幅度幾十倍的提高分佈式鎖的併發性能。
因此其實不少技術,思想都是有殊途同歸之妙的。
END
若有收穫,請幫忙轉發,您的鼓勵是做者最大的動力,謝謝!
一大波微服務、分佈式、高併發、高可用的原創系列文章正在路上
歡迎掃描下方二維碼,持續關注:
石杉的架構筆記(id:shishan100)
十餘年BAT架構經驗傾囊相授