源碼閱讀:全方位講解LongAdder

原文鏈接:https://blog.csdn.net/zqz_zqz/article/details/70665941

高併發下計數功能最好的數據結構就是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次,結果如下: 

270369 
67634689 
-1647531835 
307599695 
-1896278063 
745495504 
632435482 
435756210 
2005365029 
-1378868364
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章