原始碼閱讀:全方位講解LongAdder
高併發下計數功能最好的資料結構就是LongAdder與DoubleAdder,低併發下效率也非常優秀,這是我見過的java併發包中設計的最為巧妙的類,從軟硬體方面將java併發累加操作優化到了極致,所以應該我們應該弄清楚它的每一行程式碼為什麼要這樣做,它倆的實現大同小異,下面以LongAdder類為例介紹下它的實現。
Striped64類
public class LongAdder extends Striped64 implements Serializable
LongAdder繼承了Striped64類,來實現累加功能的,它是實現高併發累加的工具類;
Striped64的設計核心思路就是通過內部的分散計算來避免競爭。
Striped64內部包含一個base和一個Cell[] cells陣列,又叫hash表。
沒有競爭的情況下,要累加的數通過cas累加到base上;如果有競爭的話,會將要累加的數累加到Cells陣列中的某個cell元素裡面。所以整個Striped64的值為sum=base+∑[0~n]cells。
Striped64內部三個重要的成員變數:
/**
* 存放Cell的hash表,大小為2的冪。
*/
transient volatile Cell[] cells;
/**
* 基礎值,
* 1. 在沒有競爭時會更新這個值;
* 2. 在cells初始化的過程中,cells處於不可用的狀態,這時候也會嘗試將通過cas操作值累加到base。
*/
transient volatile long base;
/**
* 自旋鎖,通過CAS操作加鎖,用於保護建立或者擴充套件Cell表。
*/
transient volatile int cellsBusy;
成員變數cells
cells陣列是LongAdder高效能實現的必殺器:
AtomicInteger只有一個value,所有執行緒累加都要通過cas競爭value這一個變數,高併發下執行緒爭用非常嚴重;
而LongAdder則有兩個值用於累加,一個是base,它的作用類似於AtomicInteger裡面的value,在沒有競爭的情況不會用到cells陣列,它為null,這時使用base做累加,有了競爭後cells陣列就上場了,第一次初始化長度為2,以後每次擴容都是變為原來的兩倍,直到cells陣列的長度大於等於當前伺服器cpu的數量為止就不在擴容(想下為什麼到超過cpu數量的時候就不再擴容);每個執行緒會通過執行緒對cells[threadLocalRandomProbe%cells.length]位置的Cell物件中的value做累加,這樣相當於將執行緒繫結到了cells中的某個cell物件上;
成員變數cellsBusy
cellsBusy,它有兩個值0 或1,它的作用是當要修改cells陣列時加鎖,防止多執行緒同時修改cells陣列,0為無鎖,1為加鎖,加鎖的狀況有三種
1. cells陣列初始化的時候;
2. cells陣列擴容的時候;
3. 如果cells陣列中某個元素為null,給這個位置建立新的Cell物件的時候;
成員變數base
它有兩個作用:
1. 在開始沒有競爭的情況下,將累加值累加到base
2. 在cells初始化的過程中,cells不可用,這時會嘗試將值累加到base上;
Cell內部類
//為提高效能,使用註解@sun.misc.Contended,用來避免偽共享,
@sun.misc.Contended static final class Cell {
//用來儲存要累加的值
volatile long value;
Cell(long x) { value = x; }
//使用UNSAFE類的cas來更新value值
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
private static final sun.misc.Unsafe UNSAFE;
//value在Cell類中儲存位置的偏移量;
private static final long valueOffset;
//這個靜態方法用於獲取偏移量
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> ak = Cell.class;
valueOffset = UNSAFE.objectFieldOffset
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
這個類很簡單,final型別,內部有一個value值,使用cas來更新它的值;Cell類唯一需要注意的地方就是Cell類的註解@sun.misc.Contended。
偽共享
要理解Contended註解的作用,要先弄清楚什麼是偽共享,會有什麼影響,如何解決偽共享。
快取行cache line
要理解偽共享先要弄清楚什麼是cache line,cpu的快取系統中是以快取行(cache line)為單位儲存的,快取行是2的整數冪個連續位元組,一般為32-256個位元組。最常見的快取行大小是64個位元組,cache line是cache和memory之間資料傳輸的最小單元。
大多數現代cpu都one-die了L1和L2cache。對於L1 cache,大多是write though的;L2 cache則是write back的,不會立即寫回memory,這就會導致cache和memory的內容的不一致;另外,對於mp(multi processors)的環境,由於cache是cpu私有的,不同cpu的cache的內容也存在不一致的問題,因此很多mp的的計算架構,不論是ccnuma還是smp都實現了cache coherence的機制,即不同cpu的cache一致性機制。
Write-through(直寫模式)在資料更新時,同時寫入快取Cache和後端儲存。此模式的優點是操作簡單;缺點是因為資料修改需要同時寫入儲存,資料寫入速度較慢。
Write-back(回寫模式)在資料更新時只寫入快取Cache。只在資料被替換出快取時,被修改的快取資料才會被寫到後端儲存。此模式的優點是資料寫入速度快,因為不需要寫儲存;缺點是一旦更新後的資料未被寫入儲存時出現系統掉電的情況,資料將無法找回。
cache coherence的一種實現是通過cache-snooping協議,每個cpu通過對bus的snoop實現對其它cpu讀寫cache的監控:
- 當cpu1要寫cache時,其它cpu就會檢查自己cache中對應的cache line,如果是dirty的,就write back到memory,並且會將cpu1的相關cache line重新整理;如果不是dirty的,就invalidate該cache line.
- 當cpu1要讀cache時,其它cpu就會將自己cache中對應的cache line中標記為dirty的部分write back到memory,並且會將cpu1的相關cache line重新整理。
所以,提高cpu的cache hit rate,減少cache和memory之間的資料傳輸,將會提高系統的效能。
因此,在程式和二進位制物件的記憶體分配中保持cache line aligned就十分重要,如果不保證cache line對齊,出現多個cpu中並行執行的程式或者執行緒同時讀寫同一個cache line的情況的概率就會很大。這時cpu的cache和memory之間會反覆出現write back和refresh情況,這種情形就叫做cache thrashing。
為了有效的避免cache thrashing,通常有以下兩種途徑:
- 對於heap的分配,很多系統在malloc呼叫中實現了強制的alignment.
- 對於stack的分配,很多編譯器提供了stack aligned的選項。
當然,如果在編譯器指定了stack aligned,程式的尺寸將會變大,會佔用更多的記憶體。因此,這中間的取捨需要仔細考慮;
為了解決這個問題在jdk1.6會採用long padding的方式,就是在防止被偽共享的變數的前後加上7個long型別的變數,如下所示:
public class VolatileLongPadding {
volatile long p0, p1, p2, p3, p4, p5, p6;
volatile long v = 0L;
volatile long q0, q1, q2, q3, q4, q5, q6;
}
jdk1.7的某個版本後會優化掉long padding,為了解決這個問題,在jdk1.8中加入了@sun.misc.Contended;
LongAdder
前面說了一大堆,現在終於進入到正題了。
LongAdder –>add方法
add方法是LongAdder累加的方法,傳入的引數x為要累加的值;
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
/**
* 如果一下兩種條件則繼續執行if內的語句
* 1. cells陣列不為null(不存在爭用的時候,cells陣列一定為null,一旦對base的cas操作失敗,才會初始化cells陣列)
* 2. 如果cells陣列為null,如果casBase執行成功,則直接返回,如果casBase方法執行失敗(casBase失敗,說明第一次爭用衝突產生,需要對cells陣列初始化)進入if內;
* casBase方法很簡單,就是通過UNSAFE類的cas設定成員變數base的值為base+要累加的值
* casBase執行成功的前提是無競爭,這時候cells陣列還沒有用到為null,可見在無競爭的情況下是類似於AtomticInteger處理方式,使用cas做累加。
*/
if ((as = cells) != null || !casBase(b = base, b + x)) {
//uncontended判斷cells陣列中,當前執行緒要做cas累加操作的某個元素是否#不#存在爭用,如果cas失敗則存在爭用;uncontended=false代表存在爭用,uncontended=true代表不存在爭用。
boolean uncontended = true;
/**
*1. as == null : cells陣列未被初始化,成立則直接進入if執行cell初始化
*2. (m = as.length - 1) < 0: cells陣列的長度為0
*條件1與2都代表cells陣列沒有被初始化成功,初始化成功的cells陣列長度為2;
*3. (a = as[getProbe() & m]) == null :如果cells被初始化,且它的長度不為0,則通過getProbe方法獲取當前執行緒Thread的threadLocalRandomProbe變數的值,初始為0,然後執行threadLocalRandomProbe&(cells.length-1 ),相當於m%cells.length;如果cells[threadLocalRandomProbe%cells.length]的位置為null,這說明這個位置從來沒有執行緒做過累加,需要進入if繼續執行,在這個位置建立一個新的Cell物件;
*4. !(uncontended = a.cas(v = a.value, v + x)):嘗試對cells[threadLocalRandomProbe%cells.length]位置的Cell物件中的value值做累加操作,並返回操作結果,如果失敗了則進入if,重新計算一個threadLocalRandomProbe;
如果進入if語句執行longAccumulate方法,有三種情況
1. 前兩個條件代表cells沒有初始化,
2. 第三個條件指當前執行緒hash到的cells陣列中的位置還沒有其它執行緒做過累加操作,
3. 第四個條件代表產生了衝突,uncontended=false
**/
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
longAccumulate方法
三個引數第一個為要累加的值,第二個為null,第三個為wasUncontended表示呼叫方法之前的add方法是否未發生競爭;
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
//獲取當前執行緒的threadLocalRandomProbe值作為hash值,如果當前執行緒的threadLocalRandomProbe為0,說明當前執行緒是第一次進入該方法,則強制設定執行緒的threadLocalRandomProbe為ThreadLocalRandom類的成員靜態私有變數probeGenerator的值,後面會詳細將hash值的生成;
//另外需要注意,如果threadLocalRandomProbe=0,代表新的執行緒開始參與cell爭用的情況
//1.當前執行緒之前還沒有參與過cells爭用(也許cells陣列還沒初始化,進到當前方法來就是為了初始化cells陣列後爭用的),是第一次執行base的cas累加操作失敗;
//2.或者是在執行add方法時,對cells某個位置的Cell的cas操作第一次失敗,則將wasUncontended設定為false,那麼這裡會將其重新置為true;第一次執行操作失敗;
//凡是參與了cell爭用操作的執行緒threadLocalRandomProbe都不為0;
int h;
if ((h = getProbe()) == 0) {
//初始化ThreadLocalRandom;
ThreadLocalRandom.current(); // force initialization
//將h設定為0x9e3779b9
h = getProbe();
//設定未競爭標記為true
wasUncontended = true;
}
//cas衝突標誌,表示當前執行緒hash到的Cells陣列的位置,做cas累加操作時與其它執行緒發生了衝突,cas失敗;collide=true代表有衝突,collide=false代表無衝突
boolean collide = false;
for (;;) {
Cell[] as; Cell a; int n; long v;
//這個主幹if有三個分支
//1.主分支一:處理cells陣列已經正常初始化了的情況(這個if分支處理add方法的四個條件中的3和4)
//2.主分支二:處理cells陣列沒有初始化或者長度為0的情況;(這個分支處理add方法的四個條件中的1和2)
//3.主分支三:處理如果cell陣列沒有初始化,並且其它執行緒正在執行對cells陣列初始化的操作,及cellbusy=1;則嘗試將累加值通過cas累加到base上
//先看主分支一
if ((as = cells) != null && (n = as.length) > 0) {
/**
*內部小分支一:這個是處理add方法內部if分支的條件3:如果被hash到的位置為null,說明沒有執行緒在這個位置設定過值,沒有競爭,可以直接使用,則用x值作為初始值建立一個新的Cell物件,對cells陣列使用cellsBusy加鎖,然後將這個Cell物件放到cells[m%cells.length]位置上
*/
if ((a = as[(n - 1) & h]) == null) {
//cellsBusy == 0 代表當前沒有執行緒cells陣列做修改
if (cellsBusy == 0) {
//將要累加的x值作為初始值建立一個新的Cell物件,
Cell r = new Cell(x);
//如果cellsBusy=0無鎖,則通過cas將cellsBusy設定為1加鎖
if (cellsBusy == 0 && casCellsBusy()) {
//標記Cell是否建立成功並放入到cells陣列被hash的位置上
boolean created = false;
try {
Cell[] rs; int m, j;
//再次檢查cells陣列不為null,且長度不為空,且hash到的位置的Cell為null
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
//將新的cell設定到該位置
rs[j] = r;
created = true;
}
} finally {
//去掉鎖
cellsBusy = 0;
}
//生成成功,跳出迴圈
if (created)
break;
//如果created為false,說明上面指定的cells陣列的位置cells[m%cells.length]已經有其它執行緒設定了cell了,繼續執行迴圈。
continue;
}
}
//如果執行的當前行,代表cellsBusy=1,有執行緒正在更改cells陣列,代表產生了衝突,將collide設定為false
collide = false;
/**
*內部小分支二:如果add方法中條件4的通過cas設定cells[m%cells.length]位置的Cell物件中的value值設定為v+x失敗,說明已經發生競爭,將wasUncontended設定為true,跳出內部的if判斷,最後重新計算一個新的probe,然後重新執行迴圈;
*/
} else if (!wasUncontended)
//設定未競爭標誌位true,繼續執行,後面會算一個新的probe值,然後重新執行迴圈。
wasUncontended = true;
/**
*內部小分支三:新的爭用執行緒參與爭用的情況:處理剛進入當前方法時threadLocalRandomProbe=0的情況,也就是當前執行緒第一次參與cell爭用的cas失敗,這裡會嘗試將x值加到cells[m%cells.length]的value ,如果成功直接退出
*/
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
/**
*內部小分支四:分支3處理新的執行緒爭用執行失敗了,這時如果cells陣列的長度已經到了最大值(大於等於cup數量),或者是當前cells已經做了擴容,則將collide設定為false,後面重新計算prob的值
else if (n >= NCPU || cells != as)
collide = false;
/**
*內部小分支五:如果發生了衝突collide=false,則設定其為true;會在最後重新計算hash值後,進入下一次for迴圈
*/
else if (!collide)
//設定衝突標誌,表示發生了衝突,需要再次生成hash,重試。 如果下次重試任然走到了改分支此時collide=true,!collide條件不成立,則走後一個分支
collide = true;
/**
*內部小分支六:擴容cells陣列,新參與cell爭用的執行緒兩次均失敗,且符合庫容條件,會執行該分支
*/
else if (cellsBusy == 0 && casCellsBusy()) {
try {
//檢查cells是否已經被擴容
if (cells == as) { // Expand table unless stale
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
//為當前執行緒重新計算hash值
h = advanceProbe(h);
//這個大的分支處理add方法中的條件1與條件2成立的情況,如果cell表還未初始化或者長度為0,先嚐試獲取cellsBusy鎖。
}else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
//初始化cells陣列,初始容量為2,並將x值通過hash&1,放到0個或第1個位置上
if (cells == as) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
//解鎖
cellsBusy = 0;
}
//如果init為true說明初始化成功,跳出迴圈
if (init)
break;
}
/**
*如果以上操作都失敗了,則嘗試將值累加到base上;
*/
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
關於hash的生成
hash是LongAdder定位當前執行緒應該將值累加到cells陣列哪個位置上的,所以hash的演算法是非常重要的,下面就來看看它的實現。
java的Thread類裡面有一個成員變數
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;
threadLocalRandomProbe這個變數的值就是LongAdder用來hash定位Cells陣列位置的,平時執行緒的這個變數一般用不到,它的值一直都是0。
在LongAdder的父類Striped64裡通過getProbe方法獲取當前執行緒threadLocalRandomProbe的值:
static final int getProbe() {
//PROBE是threadLocalRandomProbe變數在Thread類裡面的偏移量,所以下面語句獲取的就是threadLocalRandomProbe的值;
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
threadLocalRandomProbe的初始化
執行緒對LongAdder的累加操作,在沒有進入longAccumulate方法前,threadLocalRandomProbe一直都是0,當發生爭用後才會進入longAccumulate方法中,進入該方法第一件事就是判斷threadLocalRandomProbe是否為0,如果為0,則將其設定為0x9e3779b9
int h;
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current();
h = getProbe();
//設定未競爭標記為true
wasUncontended = true;
}
重點在這行ThreadLocalRandom.current();
public static ThreadLocalRandom current() {
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
在current方法中判斷如果probe的值為0,則執行locaInit()方法,將當前執行緒的probe設定為非0的值,該方法實現如下:
static final void localInit() {
//private static final AtomicInteger probeGenerator =
new AtomicInteger();
//private static final int PROBE_INCREMENT = 0x9e3779b9;
int p = probeGenerator.addAndGet(PROBE_INCREMENT);
//prob不能為0
int probe = (p == 0) ? 1 : p; // skip 0
long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
//獲取當前執行緒
Thread t = Thread.currentThread();
UNSAFE.putLong(t, SEED, seed);
//將probe的值更新為probeGenerator的值
UNSAFE.putInt(t, PROBE, probe);
}
probeGenerator 是static 型別的AtomicInteger類,每執行一次localInit()方法,都會將probeGenerator 累加一次0x9e3779b9這個值;,0x9e3779b9這個數字的得來是 2^32 除以一個常數,這個常數就是傳說中的黃金比例 1.6180339887;然後將當前執行緒的threadLocalRandomProbe設定為probeGenerator 的值,如果probeGenerator 為0,這取1;
threadLocalRandomProbe重新生成
就是將prob的值左右移位 、異或操作三次
static final int advanceProbe(int probe) {
probe ^= probe << 13; // xorshift
probe ^= probe >>> 17;
probe ^= probe << 5;
UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
return probe;
}
probe從=1開始反覆執行10次,結果如下:
1
270369
67634689
-1647531835
307599695
-1896278063
745495504
632435482
435756210
2005365029
-1378868364
相關文章
- LongAdder原始碼閱讀筆記原始碼筆記
- 【原始碼閱讀】AndPermission原始碼閱讀原始碼
- PostgreSQL 原始碼解讀(3)- 如何閱讀原始碼SQL原始碼
- 【原始碼閱讀】Glide原始碼閱讀之with方法(一)原始碼IDE
- 【原始碼閱讀】Glide原始碼閱讀之into方法(三)原始碼IDE
- 【詳解】ThreadPoolExecutor原始碼閱讀(三)thread原始碼
- 【詳解】ThreadPoolExecutor原始碼閱讀(二)thread原始碼
- 【詳解】ThreadPoolExecutor原始碼閱讀(一)thread原始碼
- ReactorKit原始碼閱讀React原始碼
- AQS原始碼閱讀AQS原始碼
- CountDownLatch原始碼閱讀CountDownLatch原始碼
- HashMap 原始碼閱讀HashMap原始碼
- delta原始碼閱讀原始碼
- 原始碼閱讀-HashMap原始碼HashMap
- NGINX原始碼閱讀Nginx原始碼
- Mux 原始碼閱讀UX原始碼
- HashMap原始碼閱讀HashMap原始碼
- fuzz原始碼閱讀原始碼
- RunLoop 原始碼閱讀OOP原始碼
- express 原始碼閱讀Express原始碼
- muduo原始碼閱讀原始碼
- stack原始碼閱讀原始碼
- 閱讀原始碼後,來講講React Hooks是怎麼實現的原始碼ReactHook
- 【原始碼閱讀】Glide原始碼閱讀之load方法(二)原始碼IDE
- Caddy 原始碼閱讀(一)Run 詳解原始碼
- Tomcat中session詳解(原始碼閱讀)TomcatSession原始碼
- JDK原始碼閱讀:Object類閱讀筆記JDK原始碼Object筆記
- Laravel 原始碼閱讀 - QueueLaravel原始碼
- Vollery原始碼閱讀(—)原始碼
- 使用OpenGrok閱讀原始碼原始碼
- 如何閱讀Java原始碼?Java原始碼
- buffer 原始碼包閱讀原始碼
- 原始碼閱讀技巧篇原始碼
- 如何閱讀框架原始碼框架原始碼
- 再談原始碼閱讀原始碼
- Laravel 原始碼閱讀 - EloquentLaravel原始碼
- 如何閱讀jdk原始碼?JDK原始碼
- express 原始碼閱讀(全)Express原始碼