JUC包中的分而治之策略-为提高性能而生
一、前言
本次分享我們來共同探討JUC包中一些有意思的類,包含AtomicLong & LongAdder,ThreadLocalRandom原理。
二、AtomicLong & LongAdder
2.1 AtomicLong 類
AtomicLong是JUC包提供的原子性操作類,其內部通過CAS保證了對計數的原子性更新操作。
大家可以翻看源碼發現內部是通過UnSafe(rt.jar)這個類的CAs操作來保證對內部的計數器變量 long value進行原子性更新的,比如JDK8中:
public final long incrementAndGet() {return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;}其中unsafe.getAndAddLong的代碼如下:
public final long getAndAddLong(Object paramObject, long paramLong1, long paramLong2){long l;do{l = getLongVolatile(paramObject, paramLong1);//(1)} while (!compareAndSwapLong(paramObject, paramLong1, l, l + paramLong2));//(2)return l;}可知最終調用的是native 方法compareAndSwapLong原子性操作。
當多個線程調用同一個AtomicLong實例的incrementAndGet方法后,多個線程都會執行到unsafe.getAndAddLong方法,然后多個線程都會執行到代碼(1)處獲取計數器的值,然后都會去執行代碼(2),如果多個線程同時執行了代碼(2),由于CAS具有原子性,所以只有一個線程會更新成功,然后返回true從而退出循環,整個更新操作就OK了。其他線程則CAS失敗返回false,則循環一次在次從(1)處獲取當前計數器的值,然后在嘗試執行(2),這叫做CAS的自旋操作,本質是使用Cpu 資源換取使用鎖帶來的上下文切換等開銷。
2.2 LongAdder類
AtomicLong類為開發人員使用線程安全的計數器提供了方便,但是AtomicLong在高并發下存在一些問題,如上所述,當大量線程調用同一個AtomicLong的實例的方法時候,同時只有一個線程會CAS計數器的值成功,失敗的線程則會原地占用cpu進行自旋轉重試,這回造成大量線程白白浪費cpu原地自旋轉。
在JDK8中新增了一個LongAdder類,其采用分而治之的策略來減少同一個變量的并發競爭度,LongAdder的核心思想是把一個原子變量分解為多個變量,讓同樣多的線程去競爭多個資源,這樣競爭每個資源的線程數就被分擔了下來,下面通過圖形來理解下兩者設計的不同之處:
如上圖AtomicLong是多個線程同時競爭同一個原子變量。
如上圖LongAdder內部維護多個Cell變量,在同等并發量的情況下,爭奪單個變量更新操作的線程量會減少,這是變相的減少了爭奪共享資源的并發量。
下面我們首先看下Cell的結構:
@sun.misc.Contended static final class Cell {volatile long value;Cell(long x) { value = x; }final boolean cas(long cmp, long val) {return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);}// Unsafe 機制private static final sun.misc.Unsafe UNSAFE;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);}}}LongAdder維護了一個延遲初始化的原子性更新數組(默認情況下Cell數組是null)和一個基值變量base,由于Cells占用內存是相對比較大的,所以一開始并不創建,而是在需要時候在創建,也就是惰性 創建。
當一開始判斷cell數組是null并且并發線程較少時候所有的累加操作都是對base變量進行的,這時候就退化為了AtomicLong。cell數組的大小保持是2的N次方大小,初始化時候Cell數組的中Cell的元素個數為2,數組里面的變量實體是Cell類型。
當多個線程在爭奪同一個Cell原子變量時候如果失敗并不是在當前cell變量上一直自旋CAS重試,而是會嘗試在其它Cell的變量上進行CAS嘗試,這個改變增加了當前線程重試時候CAS成功的可能性。最后獲取LongAdder當前值的時候是把所有Cell變量的value值累加后在加上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;}如上代碼可知首先把base的值賦值給sum變量,然后通過循環把每個cell元素的value值累加到sum變量上,最后返回sum.
其實這是一種分而治之的策略,先把并發量分擔到多個原子變量上,讓多個線程并發的對不同的原子變量進行操作,然后獲取計數時候在把所有原子變量的計數和累加。
思考問題:
- 何時初始化cell數組
- 當前線程如何選擇cell中的元素進行訪問
- 如果保證cell中元素更新的線程安全
- cell數組何時進行擴容,cell元素個數可以無限擴張?
性能對比,這里有一個文章?http://blog.palominolabs.com/2014/02/10/java-8-performance-improvements-longadder-vs-atomiclong/
三、 Random & ThreadLocalRandom
3.1 Random類原理及其局限性
在JDK7之前包括現在java.util.Random應該是使用比較廣泛的隨機數生成工具類,下面先通過簡單的代碼看看java.util.Random是如何使用的:
public class RandomTest {public static void main(String[] args) {//(1)創建一個默認種子的隨機數生成器Random random = new Random();//(2)輸出10個在0-5(包含0,不包含5)之間的隨機數for (int i = 0; i < 10; ++i) {System.out.println(random.nextInt(5));}} }- 代碼(1)創建一個默認隨機數生成器,使用默認的種子。
- 代碼(2)輸出輸出10個在0-5(包含0,不包含5)之間的隨機數。
如上代碼可知新的隨機數的生成需要兩個步驟
- 首先需要根據老的種子計算生成新的種子。
- 然后根據新的種子和bound變量通過一定的算法來計算新的隨機數。
下面看下next()代碼:
protected int next(int bits) {long oldseed, nextseed;AtomicLong seed = this.seed;do {//(6)獲取當前原子變量種子的值oldseed = seed.get();//(7)根據當前種子值計算新的種子nextseed = (oldseed * multiplier + addend) & mask;//(8)使用新種子替換老的種子} while (!seed.compareAndSet(oldseed, nextseed));//(9)return (int)(nextseed >>> (48 - bits));}- 代碼(6)使用原子變量的get方法獲取當前原子變量種子的值
- 代碼(7)根據具體的算法使用當前種子值計算新的種子
- 代碼(8)使用CAS操作,使用新的種子去更新老的種子,多線程下可能多個線程都同時執行到了代碼(6)那么可能多個線程都拿到的當前種子的值是同一個,然后執行步驟(7)計算的新種子也都是一樣的,但是步驟(8)的CAS操作會保證只有一個線程可以更新老的種子為新的,失敗的線程會通過循環從新獲取更新后的種子作為當前種子去計算老的種子,這就保證了隨機數的隨機性。
- 代碼(9)則使用固定算法根據新的種子計算隨機數,并返回。
3.2 ThreadLocalRandom
Random類生成隨機數原理以及不足:每個Random實例里面有一個原子性的種子變量用來記錄當前的種子的值,當要生成新的隨機數時候要根據當前種子計算新的種子并更新回原子變量。
多線程下使用單個Random實例生成隨機數時候,多個線程同時計算隨機數計算新的種子時候多個線程會競爭同一個原子變量的更新操作,由于原子變量的更新是CAS操作,同時只有一個線程會成功,那么CAS操作失敗的大量線程進行自旋重試,而大量線程的自旋重試是會降低并發性能和消耗CPU資源的,為了解決這個問題,ThreadLocalRandom類應運而生。
public class RandomTest {public static void main(String[] args) {//(10)獲取一個隨機數生成器ThreadLocalRandom random = ThreadLocalRandom.current();//(11)輸出10個在0-5(包含0,不包含5)之間的隨機數for (int i = 0; i < 10; ++i) {System.out.println(random.nextInt(5));}} }如上代碼(10)調用ThreadLocalRandom.current()來獲取當前線程的隨機數生成器。下面來分析下ThreadLocalRandom的實現原理。
從名字看會讓我們聯想到ThreadLocal類。ThreadLocal通過讓每一個線程拷貝一份變量,每個線程對變量進行操作時候實際是操作自己本地內存里面的拷貝,從而避免了對共享變量進行同步。實際上ThreadLocalRandom的實現也是這個原理。Random的缺點是多個線程會使用原子性種子變量,會導致對原子變量更新的競爭,這個原理可以通過下面圖來表達:
那么如果每個線程維護自己的一個種子變量,每個線程生成隨機數時候根據自己本地內存中的老的種子計算新的種子,并使用新種子更新老的種子,然后根據新種子計算隨機數,就不會存在競爭問題,這會大大提高并發性能,如下圖ThreadLocalRandom原理可以使用下圖表達:
Thread類里面有幾個變量:
/** The current seed for a ThreadLocalRandom */@sun.misc.Contended("tlr")long threadLocalRandomSeed;/** Probe hash value; nonzero if threadLocalRandomSeed initialized */@sun.misc.Contended("tlr")int threadLocalRandomProbe;思考問題:
- 每個線程的初始種子怎么生成的
- 如果保障多個線程產生的種子不一樣
四、總結
本文是對拙作?java并發編程之美?一書中有關章節的提煉。本次分享首先講解了AtomicLong的內部實現,以及存在的缺點,然后講解了 LongAdder采用分而治之的策略通過使用多個原子變量減小單個原子變量競爭的并發度。然后簡單介紹了Random,和其缺點,最后介紹了ThreadLocalRandom借用ThreadLocal的思想解決了多線程對同一個原子變量競爭鎖帶來的性能損耗。其實JUC包中還有其他一些經典的組件,比如fork-join框架等。
?
原文鏈接
本文為云棲社區原創內容,未經允許不得轉載。
總結
以上是生活随笔為你收集整理的JUC包中的分而治之策略-为提高性能而生的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 阿里云栖开发者沙龙PHP技术专场-聊聊服
- 下一篇: MSSQL-最佳实践-Always En