19.Atomic系列之LongAdder的底层原理(分段锁提升并发性能)
老王:小陳啊,上一章我們講解了cas的缺陷,無法同時更新多個變量、以及ABA的問題。以及如果使用AtomicReference解決同時更新多個變量,如果使用AtomicStampedReference解決ABA的問題,這些都還記得不?
小陳:嗯嗯,記得的。
老王:那好,這一章節我們就來講解CAS帶來的另外一個問題,在并發激烈的時候,產生大量的自旋,空耗CPU的問題,以及怎么使用分段鎖機制解決這個問題的,我們已LongAdder這個原子類來舉例講解。
小陳:嗯嗯,洗耳恭聽......
AtomicInteger的缺陷,并發競爭激烈時導致大量線程自旋
老王:小陳啊,在說LongAdder之前,我先問題一個問題,為什么要設計出LongAdder?
小陳:這個啊,我看有些人說在并發非常高的時候,使用AtomicInteger、AtomicLong底層的CAS操作競爭非常激烈,會導致大量線程CAS失敗而不斷自旋,耗費CPU的性能。
老王:哦,那你來說說并發非常高的時候,AtomicInteger、AtomicLong為什么會導致的大量自旋?
小陳:AtomicInteger、AtomicLong的底層原理和實現方式基本一致,只不過一個是int類型一個是long類型,我就拿我們之前討論過的AtomicInteger來分析一下:
我記得AtomicInteger的incrementAndGet方法的底層源碼是這樣的:
public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1; }底層調用了unsafe類的getAndAddInt方法,源碼如下:
public final int getAndAddInt(Object o, long valueOffset, int x) {int expected;// CAS的失敗的線程,會不斷在do... while循環里重試,知道成功為止do {expected = this.getIntVolatile(o, valueOffset);// while條件判斷這里的compareAndSwapInt每次只有一個線程成功} while(!this.compareAndSwapInt(o, valueOffset, expected, expected + x));return expected; }(1)我們看一下上述 do...while 循環,假設有10000個線程并發的操作,由于CAS操作能保證原子性(之前的文章講過哦,CAS的底層原理),一個時刻只會有一個線程執行compareAndSwapInt方法成功
(2)失敗的另外9999個線程,進入下一次循環,然后成功一個,剩下9998個進入下一層循環....,這種方式啊,競爭太過激烈導致大量線程在循環里面自旋重試,此時是不會釋放CPU資源的,大大降低CPU的利用率,降低了并發的性能。
小陳:我再畫一副圖補充一下:
小陳:上面就是我理解競爭非常激烈的時候,大量線程在do...while循環里面自旋重試,導致非常消耗CPU資源,降低了并發的性能。
老王:很好很好,理解得非常不錯,看來小陳你對之前講解的AtomicInteger理解很深入了,哈哈,不愧是我親自帶出來的......
小陳:哪能啊,嘿嘿,都是老王你教的好啊......
老王:小陳啊,既然你知道了AtomicInteger在并發競爭非常激烈會導致大量線程自旋,那你說說LongAdder在設計層次是怎么解決這個問題的?
小陳:額,我印象中LongAdder采用分段鎖的思想,去減少并發競爭的;我打個比方還是上面10000個線程并發操作,但是LongAdder內部可能有10個鎖,不同的線程可能去競爭不同的鎖,平均下來可能是1000個線程競爭1個鎖這樣;并發性能這樣比起AtomicInteger可能就提升了10倍。
老王:你說的大概準確,但是你能說說什么是分段鎖嗎?LongAdder底層又是怎么實現分段鎖的?
小陳:額,這個,我就不太懂了,還是老王你來說吧......
老王:好,那我先給你講講什么是分段鎖吧,講完分段鎖之后再給講LongAdder是怎么實現分段鎖的
老王:下面我以銀行辦事大廳多窗口機制來給你將什么是分段鎖
銀行辦事大廳多窗口講解分段鎖
如下圖所示:
(1)銀行大廳有一個常規窗口,一堆備用窗口;每個窗口有個計數器value記錄了當前窗口的訪客人數
(2)當人數很少的時候,不存在競爭,通常辦事大廳只啟用常規窗口就足以應對了
(3)當銀行某個經理想知道總的訪客人數的時候,需要將所有窗口的value訪問人數累加就可以了
老王:上面的圖看懂了沒?
小陳:看懂了,也就是說一個銀行辦事大廳,處理訪客的請求。當訪客人數很少,比如一兩個的時候只開放常規窗口就可以了。
同時每個窗口內部有一個訪客計數器,窗口每次接待完一個訪客,value就加1,當銀行經理想看一下整個辦事大廳的來訪總人數的時候,就把所有的窗口的訪客計數器value累加就可以了。
老王:沒錯,就是這樣。下面再給你說說,當訪客非常多的時候,怎么使用備用窗口減少競爭的?
多窗口處理減少競爭
(1)在訪客非常多的情況下,只開啟常規窗口是不行的,由于一個窗口同一時間只能處理一個訪客的請求,并發競爭非常激烈(這個就是上述說的AtomicInteger的缺陷,所有人競爭通過一個窗口)
(2)所以這個時候啟用備用窗口,假如說有4個備用窗口,分別為A、B、C、D窗口,每個用戶自己有一個id,比如用戶2(id=4)來了之后看到常規窗口有人了,就會根據 id % 備用窗口大小 =》 4 % 4 = 0,就會派到第一個備用窗口,也就是窗口A
(3)同時并發非常激烈的時候,同一個備用窗口也是存在競爭的,比如用戶3(id=5)、用戶6(id=9)根據 id % 備用窗口大小 = 2,所以都競爭備用窗口B;用戶4(id=6)和用戶7(id=10)同樣會競爭備用窗口C
(4)使用了備用窗口列表,相當于將不同的用戶派遣到不同的窗口,減少了競爭,這樣并發性能自然就提升上去了。?
老王:上面的這種機制啊,其實就是類似將鎖拆成鎖多個段;其中每個窗口就是一段鎖,只有被派到同一個窗口的用戶才會存在競爭,使用分段鎖大大減少了競爭,提升了并發性能。
這個時候如果銀行經理想要知道辦事大廳的總訪問人數,只需要將多個窗口(多個段)內的value人數累加起來就可以了。
老王:小陳啊,上面通過辦事大廳多窗口(多段)機制講解分段鎖,你聽懂了沒?
小陳:牛逼啊老王,你這么講解我完全聽得懂。
老王:好了,下面再給你講講LongAdder的底層原理和怎么使用分段鎖優化并發性能的?
LongAdder屬性
老王:首先啊,我給你說LongAdder之前得給你介紹一下Striped64這個類,Striped64這個類是分段鎖的基礎,實現分段鎖的功能,而LongAdder繼承Striped64,只是在Striped64基礎之上做了簡單的封裝而已。
首先介紹一下LongAdder繼承Striped64之后內部有哪些屬性:
Cell單元(窗口)
Cell其實就是我們上面說窗口,每個窗口內部有一個value計數器
Cell {// 計數器,當前訪問人數多少volatile long value;// 構造函數Cell(long x) { value = x; }// CAS競爭同一個窗口,可能多個用戶CAS競爭這個窗口 final boolean cas(long cmp, long val) {return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);} }基礎窗口
基礎窗口直接用一個base變量表示
transient volatile long base;爭搶基礎窗口的方法,casBase底層源碼,直接cas修改base變量在內存的值
final boolean casBase(long cmp, long val) {return UNSAFE.compareAndSwapLong(this, BASE, cmp, val); }用戶id生成方法
這里就是通過線程id,經過系列運算得到用戶id
static final int getProbe() {return UNSAFE.getInt(Thread.currentThread(), PROBE); }LongAdder內部源碼分析
老王:上面知道了LongAdder內部有用哪些屬性,接下來我們看一下LongAdder內部的源碼。
老王:我們首先看下LongAdder提供的加減方法:
public void increment() {add(1L); }public void decrement() {add(-1L); }我們看到,increment和decrement方法底層調用的都是add方法,只不過傳入的是正數和負數的區別而已。
老王:看一下設計極為簡潔和精妙的add方法:
public void add(long x) {// as就類似我們上述說的備用窗口列表Cell[] as;// 這里的b就是常規窗口的值,v是你被分派到的那個窗口的value值long b, v;// m是備用窗口的長度-1,// 上面我們講過getProbe()方法就是獲取用戶id的方法// getProbe() & 其實就是 用戶id % 窗口總數,窗口分派的算法int m;// a就是你被派遣到的那個窗口Cell a;// 1.首先如果cells==null,說明備用窗口沒有開放,// 全都執行casBase爭搶常規窗口,cas成功則爭搶成功,然后辦完事就退出了// 如果爭搶失敗 casBase == false,則會進入if代碼內重試// 2. 如果cells != null,說明備用窗口開發了,不用去爭搶常規窗口了,// 直接就進入爭搶備用窗口if ((as = cells) != null || !casBase(b = base, b + x)) {boolean uncontended = true;// 3. as == null || as.length - 1 < 0 說明備用窗口列表尚未開放if (as == null || (m = as.length - 1) < 0 ||// 4. as[getProbe() & m] 是你被派遣到的那個備用窗口// (a = as[getProbe() & m]) == null 你被派遣到的那個備用窗口還沒有人來(a = as[getProbe() & m]) == null ||// 5. a.cas() 就是你被分派到窗口a后,去嘗試爭搶窗口a的權限// 如果 uncontented就是你爭搶的結果,如果!uncnotented == true,說明你爭搶失敗了!(uncontended = a.cas(v = a.value, v + x)))// 相當于上面操作都失敗之后的一種兜底方案longAccumulate(x, null, uncontended);} }老王:小陳啊,上面的LongAdder原子類的add方法源碼,經過我加上的一些注釋,你能拿看懂不?
小陳:看不懂......
老王:那這樣,我再畫個流程圖,結合代碼給你講解一下:
老王:小陳啊,上面就是LongAdder底層執行加減運算的流程圖了。
小陳:哇撒,這個有點小復雜啊,這幾行代碼里面包含這么多的操作啊......,我要多看幾遍才行啊。(一次看不懂的童鞋,記得多看幾遍哈)
老王:是啊,寫LongAdder這哥們看來境界十分高。
老王:按照我們的修煉方式,你一點點的從《筑基》、《練氣》、《結丹》....,你可以慢慢提高水平,看懂他們的代碼,了解他們設計的思想,前提是你要好好學啊。
老王:好了,小陳,再給十分鐘的時間,好好消化一下上面我們講解下面的內容......
小陳:好的老王,遵命......(十分鐘之后......)
老王:小陳啊,上面討論的LongAdder執行add操作的流程全部弄懂了沒?
小陳:longAccumulate的那個底層兜底的實現機制(下一章節講解),我都基本搞明白了:
(1)大概就是備用窗口沒有開放的時候,所有人都來競爭常規窗口
(2)備用窗口列表開放之后,根據算法看自己被派遣到哪個窗口,比如說窗口a
(3)然后看一下自己被派遣到的窗口a有沒有人在工作,沒有那就只等進入兜底方案等人來了才能處理了
(4)如果窗口a有人,這個時候就去競爭窗口a的權限;如果成功了,說明處理了自己的請求,那我就完事了
(5)如果去窗口a競爭失敗了,則說明別人比你先一步獲取了窗口a的權限,這個時候你只等進入兜底方案等待了。
老王:沒錯沒錯,總結得很好,看來小陳你隨著境界的提升啊,理解越來越深入了,目前我看你現在的并發實力,已經快《結丹》了,繼續保持下去...
小陳:哈哈,這都是老王你的功勞啊,都是你教的好......
老王:哈哈,好了,互吹結束;我們接著繼續。
LongAdder中獲取總數源碼分析
再來看下LongAdder的longValue和intValue的底層源碼:
這個就是相當于獲取當前辦事大廳的來訪總人數了,底層都是調用sum方法:
public long longValue() {return sum(); }public int intValue() {return (int)sum(); }再來看下sum() 方法的源碼是怎么統計所有的來訪人數的:
public long sum() {// as = cells 表示備用窗口列表Cell[] as = cells;Cell a;// base,也就是常規窗口的訪客人數long sum = base;if (as != null) {// 這里就是將 常規窗口訪客 + 所有備用窗口訪客 就是總訪客for (int i = 0; i < as.length; ++i) {if ((a = as[i]) != null)sum += a.value;}}return sum; }老王:這個底層就很簡單了,就是遍歷所有的窗口,將他們的訪問人數加起來就是了:
老王:上面的獲取總數的sum方法對你來說應該不難吧,小陳。
小陳:哈哈,這個就很簡單了,只是常規的加和操作而已......
老王:好了,今天我們分析LongAdder的底層和源碼就到了這里,我們明天分析一下Strimed64的分段加鎖實現機制,也就是我們今天圖里面的longAccumulate兜底機制。
小陳:好的老王,我們下一章見。
關注小陳,公眾號上更多更全的文章
JAVA并發文章目錄(公眾號)
JAVA并發專題 《筑基篇》
1.什么是CPU多級緩存模型?
2.什么是JAVA內存模型?
3.線程安全之可見性、有序性、原子性是什么?
4.什么是MESI緩存一致性協議?怎么解決并發的可見性問題?
JAVA并發專題《練氣篇》
5.volatile怎么保證可見性?
6.什么是內存屏障?具有什么作用?
7.volatile怎么通過內存屏障保證可見性和有序性?
8.volatile為啥不能保證原子性?
9.synchronized是個啥東西?應該怎么使用?
10.synchronized底層之monitor、對象頭、Mark Word?
11.synchronized底層是怎么通過monitor進行加鎖的?
12.synchronized的鎖重入、鎖消除、鎖升級原理?無鎖、偏向鎖、輕量級鎖、自旋、重量級鎖
13.synchronized怎么保證可見性、有序性、原子性?
JAVA并發專題《結丹篇》
14. JDK底層Unsafe類是個啥東西?
15.unsafe類的CAS是怎么保證原子性的?
16.Atomic原子類體系講解
17.AtomicInteger、AtomicBoolean的底層原理
18.AtomicReference、AtomicStampReference底層原理
19.Atomic中的LongAdder底層原理之分段鎖機制
20.Atmoic系列Strimped64分段鎖底層實現源碼剖析
JAVA并發專題《金丹篇》
21.AQS是個啥?為啥說它是JAVA并發工具基礎框架?
22.基于AQS的互斥鎖底層源碼深度剖析
23.基于AQS的共享鎖底層源碼深度剖析
24.ReentrantLock是怎么基于AQS實現獨占鎖的?
25.ReentrantLock的Condition機制底層源碼剖析
26.CountDownLatch 門栓底層源碼和實現機制深度剖析
27.CyclicBarrier 柵欄底層源碼和實現機制深度剖析
28.Semaphore 信號量底層源碼和實現機深度剖析
29.ReentrantReadWriteLock 讀寫鎖怎么表示?
30. ReentrantReadWriteLock 讀寫鎖底層源碼和機制深度剖析
JAVA并發專題《元神篇》并發數據結構篇
31.CopyOnAarrayList 底層分析,怎么通過寫時復制副本,提升并發性能?
32.ConcurrentLinkedQueue 底層分析,CAS 無鎖化操作提升并發性能?
33.ConcurrentHashMap詳解,底層怎么通過分段鎖提升并發性能?
34.LinkedBlockedQueue 阻塞隊列怎么通過ReentrantLock和Condition實現?
35.ArrayBlockedQueued 阻塞隊列實現思路竟然和LinkedBlockedQueue一樣?
36.DelayQueue 底層源碼剖析,延時隊列怎么實現?
37.SynchronousQueue底層原理解析
JAVA并發專題《飛升篇》線程池底層深度剖析
38. 什么是線程池?看看JDK提供了哪些默認的線程池?底層竟然都是基于ThreadPoolExecutor的?
39.ThreadPoolExecutor 構造函數有哪些參數?這些參數分別表示什么意思?
40.內部有哪些變量,怎么表示線程池狀態和線程數,看看道格.李大神是怎么設計的?
41. ThreadPoolExecutor execute執行流程?怎么進行任務提交的?addWorker方法干了啥?什么是workder?
42. ThreadPoolExecutor execute執行流程?何時將任務提交到阻塞隊列? 阻塞隊列滿會發生什么?
43. ThreadPoolExecutor 中的Worker是如何執行提交到線程池的任務的?多余Worker怎么在超出空閑時間后被干掉的?
44. ThreadPoolExecutor shutdown、shutdownNow內部核心流程
45. 再回頭看看為啥不推薦Executors提供幾種線程池?
46. ThreadPoolExecutor線程池篇總結
總結
以上是生活随笔為你收集整理的19.Atomic系列之LongAdder的底层原理(分段锁提升并发性能)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: STM32F103+VL53L0X寄存器
- 下一篇: Windows下pig-0.17启动时遇