subject:AtomicInteger 在高并發(fā)下性能不好,如何解決檀葛?為什么玩祟?
在 JDK1.5 中新增了并發(fā)情況下使用的 Integer/Long 所對(duì)應(yīng)的原子類 AtomicInteger 和 AtomicLong。
在并發(fā)的場(chǎng)景下屿聋,如果需要實(shí)現(xiàn)計(jì)數(shù)器空扎,可以利用 AtomicInteger 和 AtomicLong,這樣一來润讥,就可以避免加鎖和復(fù)雜的代碼邏輯转锈,有了它們之后,我們只需要執(zhí)行對(duì)應(yīng)的封裝好的方法楚殿,例如對(duì)這兩個(gè)變量進(jìn)行原子的增操作或原子的減操作撮慨,就可以滿足大部分業(yè)務(wù)場(chǎng)景的需求。
不過脆粥,雖然它們很好用砌溺,但是如果業(yè)務(wù)場(chǎng)景是并發(fā)量很大的,那么這兩個(gè)原子類實(shí)際上會(huì)有較大的性能問題变隔,這是為什么呢抚吠?
1、AtomicLong 存在的問題
首先來看一段代碼:
/**
* 描述: 在16個(gè)線程下使用AtomicLong
*/
public class AtomicLongDemo {
public static void main(String[] args) throws InterruptedException {
AtomicLong counter = new AtomicLong(0);
ExecutorService service = Executors.newFixedThreadPool(16);
for (int i = 0; i < 100; i++) {
service.submit(new Task(counter));
}
Thread.sleep(2000);
System.out.println(counter.get());
}
static class Task implements Runnable {
private final AtomicLong counter;
public Task(AtomicLong counter) {
this.counter = counter;
}
@Override
public void run() {
counter.incrementAndGet();
}
}
}
在這段代碼中可以看出弟胀,新建了一個(gè)原始值為 0 的 AtomicLong。然后,有一個(gè)線程數(shù)為 16 的線程池孵户,并且往這個(gè)線程池中添加了 100 次相同的一個(gè)任務(wù)萧朝。
在下面的 Task 類中可以看到,這個(gè)任務(wù)實(shí)際上就是每一次去調(diào)用 AtomicLong 的 incrementAndGet 方法夏哭,相當(dāng)于一次自加操作检柬。這樣一來,整個(gè)類的作用就是把這個(gè)原子類從 0 開始竖配,添加 100 個(gè)任務(wù)何址,每個(gè)任務(wù)自加一次。
這段代碼的運(yùn)行結(jié)果毫無疑問是 100进胯,雖然是多線程并發(fā)訪問用爪,但是 AtomicLong 依然可以保證 incrementAndGet 操作的原子性,所以不會(huì)發(fā)生線程安全問題胁镐。
不過如果深入一步去看內(nèi)部情景的話偎血,可能會(huì)感到意外。把模型簡化成只有兩個(gè)線程在同時(shí)工作的并發(fā)場(chǎng)景盯漂,因?yàn)閮蓚€(gè)線程和更多個(gè)線程本質(zhì)上是一樣的颇玷。如圖所示:
在這個(gè)圖中,每一個(gè)線程是運(yùn)行在自己的 core 中的就缆,并且它們都有一個(gè)本地內(nèi)存是自己獨(dú)用的帖渠。在本地內(nèi)存下方,有兩個(gè) CPU 核心共用的共享內(nèi)存竭宰。
對(duì)于 AtomicLong 內(nèi)部的 value 屬性而言空郊,也就是保存當(dāng)前 AtomicLong 數(shù)值的屬性,它是被 volatile 修飾的羞延,所以它需要保證自身可見性渣淳。
這樣一來,每一次它的數(shù)值有變化的時(shí)候伴箩,它都需要進(jìn)行 flush 和 refresh入愧。比如說,如果開始時(shí)嗤谚,ctr 的數(shù)值為 0 的話棺蛛,那么如圖所示,一旦 core 1 把它改成 1 的話巩步,它首先會(huì)在左側(cè)把這個(gè) 1 的最新結(jié)果給 flush 到下方的共享內(nèi)存旁赊。然后,再到右側(cè)去往上 refresh 到核心 2 的本地內(nèi)存椅野。這樣一來终畅,對(duì)于核心 2 而言籍胯,它才能感知到這次變化。
由于競(jìng)爭(zhēng)很激烈离福,這樣的 flush 和 refresh 操作耗費(fèi)了很多資源杖狼,而且 CAS 也會(huì)經(jīng)常失敗。
2妖爷、LongAdder 帶來的改進(jìn)和原理
在 JDK 8 中又新增了 LongAdder 這個(gè)類蝶涩,這是一個(gè)針對(duì) Long 類型的操作工具類。既然已經(jīng)有了 AtomicLong絮识,為何又要新增 LongAdder 這么一個(gè)類呢绿聘?
同樣是用一個(gè)例子來說明。下面這個(gè)例子和剛才的例子很相似次舌,只不過把工具類從 AtomicLong 變成了 LongAdder熄攘。其他的不同之處還在于最終打印結(jié)果的時(shí)候,調(diào)用的方法從原來的 get 變成了現(xiàn)在的 sum 方法垃它。其他的邏輯都一樣鲜屏。
來看一下使用 LongAdder 的代碼示例:
/**
* 描述: 在16個(gè)線程下使用LongAdder
*/
public class LongAdderDemo {
public static void main(String[] args) throws InterruptedException {
LongAdder counter = new LongAdder();
ExecutorService service = Executors.newFixedThreadPool(16);
for (int i = 0; i < 100; i++) {
service.submit(new Task(counter));
}
Thread.sleep(2000);
System.out.println(counter.sum());
}
static class Task implements Runnable {
private final LongAdder counter;
public Task(LongAdder counter) {
this.counter = counter;
}
@Override
public void run() {
counter.increment();
}
}
}
代碼的運(yùn)行結(jié)果同樣是 100,但是運(yùn)行速度比剛才 AtomicLong 的實(shí)現(xiàn)要快国拇。下面解釋一下洛史,為什么高并發(fā)下 LongAdder 比 AtomicLong 效率更高。
因?yàn)?LongAdder 引入了分段累加的概念酱吝,內(nèi)部一共有兩個(gè)參數(shù)參與計(jì)數(shù):第一個(gè)叫作 base也殖,它是一個(gè)變量,第二個(gè)是 Cell[] 务热,是一個(gè)數(shù)組忆嗜。
其中的 base 是用在競(jìng)爭(zhēng)不激烈的情況下的,可以直接把累加結(jié)果改到 base 變量上崎岂。
那么捆毫,當(dāng)競(jìng)爭(zhēng)激烈的時(shí)候,就要用到 Cell[] 數(shù)組了冲甘。一旦競(jìng)爭(zhēng)激烈绩卤,各個(gè)線程會(huì)分散累加到自己所對(duì)應(yīng)的那個(gè) Cell[] 數(shù)組的某一個(gè)對(duì)象中,而不會(huì)大家共用同一個(gè)江醇。
這樣一來濒憋,LongAdder 會(huì)把不同線程對(duì)應(yīng)到不同的 Cell 上進(jìn)行修改,降低了沖突的概率陶夜,這是一種分段的理念凛驮,提高了并發(fā)性,這就和 Java 7 的 ConcurrentHashMap 的 16 個(gè) Segment 的思想類似条辟。
競(jìng)爭(zhēng)激烈的時(shí)候黔夭,LongAdder 會(huì)通過計(jì)算出每個(gè)線程的 hash 值來給線程分配到不同的 Cell 上去宏胯,每個(gè) Cell 相當(dāng)于是一個(gè)獨(dú)立的計(jì)數(shù)器,這樣一來就不會(huì)和其他的計(jì)數(shù)器干擾本姥,Cell 之間并不存在競(jìng)爭(zhēng)關(guān)系胳嘲,所以在自加的過程中,就大大減少了剛才的 flush 和 refresh扣草,以及降低了沖突的概率,這就是為什么 LongAdder 的吞吐量比 AtomicLong 大的原因颜屠,本質(zhì)是空間換時(shí)間辰妙,因?yàn)樗卸鄠€(gè)計(jì)數(shù)器同時(shí)在工作,所以占用的內(nèi)存也要相對(duì)更大一些甫窟。
那么 LongAdder 最終是如何實(shí)現(xiàn)多線程計(jì)數(shù)的呢密浑?答案就在最后一步的求和 sum 方法,執(zhí)行 LongAdder.sum() 的時(shí)候粗井,會(huì)把各個(gè)線程里的 Cell 累計(jì)求和尔破,并加上 base,形成最終的總和浇衬。代碼如下:
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
在這個(gè) sum 方法中可以看到懒构,思路非常清晰。先取 base 的值耘擂,然后遍歷所有 Cell胆剧,把每個(gè) Cell 的值都加上去,形成最終的總和醉冤。由于在統(tǒng)計(jì)的時(shí)候并沒有進(jìn)行加鎖操作秩霍,所以這里得出的 sum 不一定是完全準(zhǔn)確的,因?yàn)橛锌赡茉谟?jì)算 sum 的過程中 Cell 的值被修改了蚁阳。
3铃绒、如何選擇?AtomicLong 可否被 LongAdder 替代螺捐?
現(xiàn)在已經(jīng)了解了颠悬,為什么 AtomicLong 或者說 AtomicInteger 它在高并發(fā)下性能不好,也同時(shí)看到了性能更好的 LongAdder归粉。下面就分析一下椿疗,對(duì)它們應(yīng)該如何選擇。
在低競(jìng)爭(zhēng)的情況下糠悼,AtomicLong 和 LongAdder 這兩個(gè)類具有相似的特征,吞吐量也是相似的倔喂,因?yàn)楦?jìng)爭(zhēng)不高铝条。但是在競(jìng)爭(zhēng)激烈的情況下,LongAdder 的預(yù)期吞吐量要高得多班缰,經(jīng)過試驗(yàn),LongAdder 的吞吐量大約是 AtomicLong 的十倍埠忘,不過凡事總要付出代價(jià)脾拆,LongAdder 在保證高效的同時(shí)莹妒,也需要消耗更多的空間。
LongAdder 只提供了 add旨怠、increment 等簡單的方法,適合的是統(tǒng)計(jì)求和計(jì)數(shù)的場(chǎng)景鉴腻,場(chǎng)景比較單一迷扇,而 AtomicLong 還具有 compareAndSet 等高級(jí)方法,可以應(yīng)對(duì)除了加減之外的更復(fù)雜的需要 CAS 的場(chǎng)景爽哎。
結(jié)論:如果場(chǎng)景僅僅是需要用到加和減操作的話蜓席,那么可以直接使用更高效的 LongAdder,但如果需要利用 CAS 比如 compareAndSet 等操作的話倦青,就需要使用 AtomicLong 來完成瓮床。