AtomicLong和LongAdder的區(qū)別
前言
最近在看到不少框架里面使用到了LongAdder這個(gè)類淡诗,而并非AtomicLong,很是困惑,于是專門(mén)看了LongAdder的源碼,總結(jié)一下這兩個(gè)的區(qū)別。
AtomicLong原理
就像我們所知道的那樣,AtomicLong的原理是依靠底層的cas來(lái)保障原子性的更新數(shù)據(jù)肢簿,在要添加或者減少的時(shí)候靶剑,會(huì)使用死循環(huán)不斷地cas到特定的值,從而達(dá)到更新數(shù)據(jù)的目的池充。那么LongAdder又是使用到了什么原理?難道有比cas更加快速的方式桩引?
LongAdder原理
首先我們來(lái)看一下LongAdder有哪些方法?
可以看到和AtomicLong基本類似,同樣有增加收夸、減少等操作坑匠,那么如何實(shí)現(xiàn)原子的增加呢?
我們可以看到一個(gè)Cell的類,那這個(gè)類是用來(lái)干什么的呢?
我們可以看到Cell類的內(nèi)部是一個(gè)volatile的變量卧惜,然后更改這個(gè)變量唯一的方式通過(guò)cas厘灼。我們可以猜測(cè)到LongAdder的高明之處可能在于將之前單個(gè)節(jié)點(diǎn)的并發(fā)分散到各個(gè)節(jié)點(diǎn)的,這樣從而提高在高并發(fā)時(shí)候的效率咽瓷。
下面我們來(lái)驗(yàn)證我們的觀點(diǎn)设凹,我們接著看上圖的add方法,如果cell數(shù)組不為空茅姜,那么就嘗試更新base元素闪朱,如果更新成功,那么就直接返回钻洒。base元素在這里起到了一個(gè)什么作用呢?可以保障的是在低并發(fā)的時(shí)候和AtomicLong一樣的直接對(duì)基礎(chǔ)元素進(jìn)行更新奋姿。
??而如果cell為空或者更新base失敗,我們看接下來(lái)的那個(gè)if判斷素标,即如果as不為空并且成功更新對(duì)應(yīng)節(jié)點(diǎn)的數(shù)據(jù)称诗,則返回,否則就會(huì)進(jìn)入longAccumulate()方法头遭。
??圖有點(diǎn)大粪狼,無(wú)法截圖,直接貼源碼
for (;;) {
Cell[] as; Cell a; int n; long v;
if ((as = cells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) { //如果對(duì)應(yīng)位置沒(méi)有數(shù)據(jù)任岸,那么直接插入元素
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) //標(biāo)示沖突標(biāo)志位 再榄,進(jìn)行重試 CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale,如果已經(jīng)cell數(shù)組的大小已經(jīng)超過(guò)了CPU核數(shù)享潜,那么再擴(kuò)容沒(méi)意義了困鸥,直接重試,或者有別的線程擴(kuò)容導(dǎo)致變更了數(shù)組,設(shè)置標(biāo)示位疾就,進(jìn)行重試澜术,,避免一失敗就擴(kuò)容
else if (!collide)
collide = true;
else if (cellsBusy == 0 && casCellsBusy()) {//開(kāi)始擴(kuò)容
try {
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
}
h = advanceProbe(h); //擴(kuò)容完成以后猬腰,重新初始化要更新的索引值鸟废,盡量保障可以更新成功
}
else if (cellsBusy == 0 && cells == as && //初始化casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
上面的代碼主要有三個(gè)分支:
????1. 如果數(shù)組不為空
????2. 數(shù)據(jù)為空,則初始化
????3. 前面都更新失敗了姑荷,嘗試更新base數(shù)據(jù)
?? cellBusy是一個(gè)標(biāo)示元素盒延,只有當(dāng)修改cell數(shù)組大小或者插入元素的時(shí)候才會(huì)修改。分支二鼠冕、分支三都很簡(jiǎn)單添寺,我們來(lái)重點(diǎn)分析一下分支一。
?? 當(dāng)要更新的位置沒(méi)有元素的時(shí)候懈费,首先cas標(biāo)志位计露,防止擴(kuò)容以及插入元素,然后插入數(shù)據(jù)憎乙。如果成功直接返回票罐,否則標(biāo)示發(fā)生了沖突,然后重試泞边。如果對(duì)應(yīng)的位置有元素則更新胶坠,如果更新失敗,進(jìn)行判斷是否數(shù)組的大小已經(jīng)超過(guò)了cpu的核數(shù)繁堡,如果大于的話沈善,則意味著擴(kuò)容沒(méi)有意義。直接重試椭蹄。否則進(jìn)行擴(kuò)容闻牡,擴(kuò)容完成后,重新設(shè)置要更新的位置绳矩,盡可能保證下一次更新成功罩润。
??我們來(lái)看一下如何統(tǒng)計(jì)計(jì)數(shù)。
當(dāng)計(jì)數(shù)的時(shí)候翼馆,將base和各個(gè)cell元素里面的值進(jìn)行疊加割以,從而得到計(jì)算總數(shù)的目的。這里的問(wèn)題是在計(jì)數(shù)的同時(shí)如果修改cell元素应媚,有可能導(dǎo)致計(jì)數(shù)的結(jié)果不準(zhǔn)確严沥。
總結(jié):
LongAdder在AtomicLong的基礎(chǔ)上將單點(diǎn)的更新壓力分散到各個(gè)節(jié)點(diǎn),在低并發(fā)的時(shí)候通過(guò)對(duì)base的直接更新可以很好的保障和AtomicLong的性能基本保持一致中姜,而在高并發(fā)的時(shí)候通過(guò)分散提高了性能消玄。
缺點(diǎn)是LongAdder在統(tǒng)計(jì)的時(shí)候如果有并發(fā)更新跟伏,可能導(dǎo)致統(tǒng)計(jì)的數(shù)據(jù)有誤差。