前言
相信大部分開發人員,或多或少都看過或寫過併發編程的代碼。併發關鍵字除了Synchronized,還有另一大分支Atomic。如果大家沒聽過沒用過先看基礎篇
,如果聽過用過,請滑至底部看進階篇
,深入源碼分析。
提出問題:int線程安全嗎?
看過Synchronized相關文章的小夥伴應該知道其是不安全的,再次用代碼應驗下其不安全性:
運行結果:
在上面的例子中,我們定義一個初始值爲0的靜態變量number,再新建並運行兩個線程讓其各執行10萬次的自增操作,如果他是線程安全的,應該兩個線程執行後結果爲20萬,但是我們發現最終的結果是小於20萬的,即說明他是不安全的。
在之前Synchronized那篇文章中說過,可以在number=number+1這句代碼上下加Synchronized關鍵字實現線程安全。但是其對資源的開銷較大,所以我們今天再看下另外一種實現線程安全的方法Atomic。
Atomic基礎篇分界線
原子整數(基礎類型)
整體介紹
Atomic是jdk提供的一系列包的總稱,這個大家族包括原子整數(AtomicInteger,AtomicLong,AtomicBoolean),原子引用(AtomicReference,AtomicStampedReference,AtomicMarkableReference),原子數組(AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray),更新器(AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater)。
AtomicInteger
AtomicInteger,AtomicBoolean,AtomicLong三者功能類似,咱就以AtomicInteger爲主分析原子類。
先看下有哪些API,及其他們具體啥功能:
執行結果:
對上述int類型的例子改進
我們可以看到運行結果是正確的20萬,說明AtomicInteger的確保證了線程安全性,即在多線程的過程中,運行結果還是正確的。但是這存在一個ABA問題,下面將原子引用的時候再說,先立個flag。
源碼分析
我們以incrementAndGet方法爲例,看下底層是如何實現的,AtomicInteger類中的incrementAndGet方法調用了Unsafe類的getAndAddInt方法。
我們看下getAndAddInt方法,裏面有個循環,直接值爲compareAndSwapInt返回值爲true,才結束循環。這裏就不得不提CAS,這就是多線程安全性問題的解決方法。
CAS
線程1和線程2同事獲取了主內存變量值0,線程1加1並寫入主內存,現在主內存變量值1,線程2也加2並嘗試寫入主內存,這個時候是不能寫入主內存的,因爲會覆蓋掉線程1的操作,具體過程如下圖。
CAS是在線程2嘗試寫入內存的時候,通過比較並設置(CompareAndSet)發現現在主內存當前值爲1,和他剛開始讀取的值0不一樣,所以他會放棄本次修改,重新讀取主內存的最新值,然後再重試下線程2的具體邏輯操作,再次嘗試寫入主內存。如果這時候線程1,再次對主內存進行了修改,線程2發現現在主內存的值又和預期不一樣,所以將放棄本次修改,再次讀取主內存最新值,再次重試並嘗試寫入主內存。我們可以發現這是一個重複比較的過程,即直到和預期初始值一樣,纔會寫入主內存,否則將一直讀取重試的循環。這就是上面for循環的意義。
CAS的實現實際上利用了CPU指令來實現的,如果操作系統不支持CAS,還是會加鎖的,如果操作系統支持CAS,則使用原子性的CPU指令。
原子引用
在日常使用中,我們不止對上述基本類型進行原子操作,而是需要對一些複雜類型進行原子操作,所以需要AtomicReference。
不安全實現
先看不安全的BigDecimal類型:
運行結果如下圖,我們可以看到兩個線程,自循環1000次加1操作,最終結果應該是2000,可是結果小於2000。
安全實現-使用CAS
運行結果如下:
ABA問題及解決
在上面CAS過程中,是通過值比較來知曉是不是能夠更新成功,那如果線程1先加1再減1,這樣主內存還是原來的值,即線程2還是可以更新成功的。但是這樣邏輯錯了
,線程1已經發生了修改,線程2不能直接更新成功。
代碼:
我們看線程2對其進行了一系列操作,但是最後打印了還是true,表示可以更新成功的。這顯然不對。
那我們可以使用AtomicStampedReference,爲其添加一個版本號。線程1在剛開始讀取主內存的時候,獲取到值爲0,版本爲1,線程2也獲取到這兩個值,線程1進行加1,減1的操作的時候,版本各加1,現在主內存的值爲0,版本爲2,而線程2還拿着預計值爲0,版本爲1的數據嘗試寫入主內存,這個時候因版本不同而更新失敗。具體我們用代碼試下:
我們可以看到每次操作都會更新stamp(版本號),在最後對比的時候不僅比較值,還比較版本號,所以是不能更新成功的,false.
原子數組
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray三者類似,所以以AtomicIntegerArray爲例,我們可以將下面AtomicIntegerArray看做是AtomicInteger類型的數組,其底層很類似,就不詳細寫了。
字段更新器和原子累加器比較簡單,這裏就不說了。
Atomic進階篇分界線
LongAdder源碼分析
LongAdder使用
LongAdder是jdk1.8之後新加的,那爲什麼要加他?這個問題,下面將回答,我們先看下如何使用。
我們可以看到LongAdder的使用和AtomicLong大致相同,使用兩個線程Thread1,Thread2對number值各進行一萬次的自增操作,最後的number是正確的兩萬。
與Atomic的對比優勢
那問題來了,既然AtomicLong能夠完成對多線程下的number進行線程安全的操作,那爲什麼還要LongAdder?我們先來段代碼比較下,兩個在結果都是正確的前提下,性能方面的差距。
上述代碼對比了1個線程,10個線程,100個線程在進行100百次自增操作後,AtomicLong和LongAdder所花費的時間。通過打印語句,我們發現在最終number1和number2都正確的基礎上,LongAdder花費的時間比AtomicLong少了一個量級。
源碼分析
那爲什麼會導致這種情況,我們就要從源碼層面分析。AtomicLong爲什麼效率低?因爲如果線程數量一多,尤其在高併發的情況下,比如有100個線程同時想要對對象進行操作,肯定只有一個線程會獲取到鎖,其他99個線程可能空轉,一直循環知道線程釋放鎖。如果該線程操作完畢釋放了鎖,其他99個線程再次競爭,也只有一個線程獲取鎖,另外98個線程還是空轉,直到鎖被釋放。這樣CAS操作會浪費大量資源在空轉上,從而使得AtomicLong在線程數越來越多的情況下越來越慢。
AtomicLong是多個線程對同一個value值進行操作,導致多個線程自旋次數太多,性能降低。而LongAdder在無競爭的情況,跟AtomicLong一樣,對同一個base進行操作,當出現競爭關係時則是採用化整爲零的做法,從空間換時間,用一個數組cells,將一個value拆分進這個數組cells。多個線程需要同時對value進行操作時候,可以對線程id進行hash得到hash值,再根據hash值映射到這個數組cells的某個下標,再對該下標所對應的值進行自增操作。當所有線程操作完畢,將數組cells的所有值和無競爭值base都加起來作爲最終結果。
我們先看下LongAdder裏面的字段,發現其裏面沒有,主要是在其繼承的Stripped64類中,有下面四個主要變量。
下面是add方法開始。
從LongAdder調用Stripped64的longAccumulate方法,主要是初始化cells
,cells的擴容
,多個線程同時命中一個cell的競爭
操作。
結語
結束了,撒花。這篇主要說了Atomic的一些使用,包括Atomic原子類(AtomicInteger,AtomicLong,AtomicBoolean),Atomic原子引用(AtomicReference,AtomicStampedReference),以及1.8之後LongAdder的優勢,源碼分析。過程還穿插了一些CAS,ABA問題引入和解決方式。
參考資料
Java多線程進階(十七)—— J.U.C之atomic框架:LongAdder