JDK1.8 LongAdder 空間換時間: 比AtomicLong還高效的無鎖實現

 

咱們知道,AtomicLong的實現方式是內部有個value 變量,當多線程併發自增,自減時,均經過CAS 指令從機器指令級別操做保證併發的原子性。html

    // setup to use Unsafe.compareAndSwapLong for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    /**
     * Records whether the underlying JVM supports lockless
     * compareAndSwap for longs. While the Unsafe.compareAndSwapLong
     * method works in either case, some constructions should be
     * handled at Java level to avoid locking user-visible locks.
     */
    static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();

    /**
     * Returns whether underlying JVM supports lockless CompareAndSet
     * for longs. Called only once and cached in VM_SUPPORTS_LONG_CAS.
     */
    private static native boolean VMSupportsCS8();

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicLong.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile long value;

    /**
     * Creates a new AtomicLong with the given initial value.
     *
     * @param initialValue the initial value
     */
    public AtomicLong(long initialValue) {
        value = initialValue;
    }

    /**
     * Creates a new AtomicLong with initial value {@code 0}.
     */
    public AtomicLong() {
    }

 

先看LongAdder的add()方法:shell

    public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            boolean uncontended = true;
            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);
        }
    }

 

Cell是Striped64的一個內部類,顧名思義,Cell 表明了一個最小單元,這個單元有什麼用,稍候會說道。先看定義:數組

    /**
     * Padded variant of AtomicLong supporting only raw accesses plus CAS.
     *
     * JVM intrinsics note: It would be possible to use a release-only
     * form of CAS here, if it were provided.
     */
    @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 mechanics
        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);
            }
        }
    }

 


Cell內部有一個很是重要的value變量,而且提供了一個更新其值的cas()方法。多線程

 

回到add方法:併發

    public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            boolean uncontended = true;
            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);
        }
    }

 

這裏,我有個疑問,AtomicLong已經使用CAS指令,很是高效了(比起各類鎖),LongAdder若是仍是用CAS指令更新值,怎麼可能比AtomicLong高效了? 況且內部還這麼多判斷!!!less

這是我開始時最大的疑問,因此,我猜測,難道有比CAS指令更高效的方式出現了? 帶着這個疑問,繼續。dom

第一if 判斷,第一次調用的時候cells數組確定爲null,所以,進入casBase方法:ide

    /**
     * CASes the base field.
     */
    final boolean casBase(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
    }

 


原子更新base沒啥好說的,若是更新成功,本地調用開始返回,不然進入分支內部。高併發

何時會更新失敗? 沒錯,併發的時候,好戲開始了,AtomicLong的處理方式是死循環嘗試更新,直到成功才返回,而LongAdder則是進入這個分支。性能

分支內部,經過一個Threadlocal變量threadHashCode 獲取一個HashCode對象,該HashCode對象依然是Striped64類的內部類,看定義:

    /**
     * Returns the probe value for the current thread.
     * Duplicated from ThreadLocalRandom because of packaging restrictions.
     */
    static final int getProbe() {
        return UNSAFE.getInt(Thread.currentThread(), PROBE);
    }

 


有個code變量,保存了一個非0的隨機數隨機值。

回到add方法:

 

拿到該線程相關的HashCode對象後,獲取它的code變量,as[(n-1)&h] 這句話至關於對h取模,只不過比起取模,由於是 與 的運算因此效率更高。

計算出一個在Cells 數組中當先線程的HashCode對應的 索引位置,並將該位置的Cell 對象拿出來用CAS更新它的value值。

 

看到這裏我想應該有不少人明白爲何LongAdder會比AtomicLong更高效了,沒錯,惟一會制約AtomicLong高效的緣由是高併發,

高併發意味着CAS的失敗概率更高, 重試次數更多,越多線程重試,CAS失敗概率又越高,變成惡性循環,AtomicLong效率下降。

那怎麼解決?

 LongAdder給了咱們一個很是容易想到的解決方案:減小併發,將單一value的更新壓力分擔到多個value中去,下降單個value的 「熱度」,分段更新!!!

這樣,線程數再多也會分擔到多個value上去更新,只須要增長value就能夠下降 value的 「熱度」  AtomicLong中的 惡性循環不就解決了嗎?

cells 就是這個 「段」 cell中的value 就是存放更新值的, 這樣,當我須要總數時,把cells 中的value都累加一下不就能夠了麼!!

固然,聰明之處遠遠不只僅這裏,在看看add方法中的代碼,casBase方法可不能夠不要,直接分段更新,上來就計算 索引位置,而後更新value?

答案是很差,不是不行,由於,casBase操做等價於AtomicLong中的CAS操做,要知道,LongAdder這樣的處理方式是有壞處的,分段操做必然帶來空間上的浪費,

能夠空間換時間,可是,能不換就不換,看空間時間都節約~! 因此,casBase操做保證了在低併發時,不會當即進入分支作分段更新操做,由於低併發時,

casBase操做基本都會成功,只有併發高到必定程度了,纔會進入分支,

因此,Doug Lea對該類的說明是: 低併發時LongAdder和AtomicLong性能差很少,高併發時LongAdder更高效!

 

總結

 

1. base有沒有參與彙總?

base在調用intValue等方法的時候是會彙總

 

2. 若是cell被建立後,原來的casBase就不走了,會不會性能更差? base的順序可不能夠調換?

    剛開始我想可不能夠調換add方法中的判斷順序,好比,先作casBase的判斷?

仔細思考後認爲仍是不調換可能更好,調換後每次都要CAS一下,在高併發時,失敗概率很是高,而且是惡性循環,比起一次判斷,

後者的開銷明顯小不少,尚未反作用(上一個問題,base變量在sum時base是會被統計的,並不會丟掉base的值)。所以,不調換可能會更好。

 

3. AtomicLong可不能夠廢掉?

雖然LongAdder在空間上佔用略大,可是,它的性能已經足以說明一切了,不管是從節約空的角度仍是執行效率上,AtomicLong基本沒有優點了

 

4. guava 、netty裏面都照搬了LongAdder的實現

 

出處:

cool shell : 從LONGADDER看更高效的無鎖實現

相關文章
相關標籤/搜索