併發編程十一java8新增的併發特性

一、原子操作CAS

1.1 LongAdder

JDK1.8時,java.util.concurrent.atomic包中提供了一個新的原子類:LongAdder。
根據Oracle官方文檔的介紹,LongAdder在高併發的場景下會比它的前輩-->AtomicLong 具有更好的性能,代價是消耗更多的內存空間。

1.2 AtomicLong中的問題

AtomicLong是利用了底層的CAS操作來提供併發性的,調用了Unsafe類的getAndAddLong方法,該方法是個native方法,它的邏輯是採用自旋的方式不斷更新目標值,直到更新成功。

AtomicLong中有個內部變量value保存着實際的long值,所有的操作都是針對該變量進行。也就是說,高併發環境下,value變量其實是一個熱點,也就是N個線程競爭一個熱點。另外在併發量較低的環境下,線程衝突的概率比較小,自旋的次數不會很多。但是,高併發環境下,N個線程同時進行自旋操作,會出現大量失敗並不斷自旋的情況,此時AtomicLong的自旋會成爲瓶頸

這就是LongAdder引入的初衷——解決高併發環境下AtomicLong的自旋瓶頸問題。

1.3 LongAdder設計思路

LongAdder的基本思路就是分散熱點,將value值分散到一個數組中,不同線程會命中到數組的不同槽中,各個線程只對自己槽中的那個值進行CAS操作,這樣熱點就被分散了,衝突的概率就小很多。如果要獲取真正的long值,只要將各個槽中的變量值累加返回。

這種做法和ConcurrentHashMap中的“分段鎖”其實就是類似的思路。

對於LongAdder來說,內部有一個base變量,一個Cell[]數組。

base變量:非競態條件下,直接累加到該變量上。

Cell[]數組:競態條件下,累加個各個線程自己的槽Cell[i]中。

 

所以,最終結果的計算應該是

 public long sum() {
        //獲取cells數據
        Cell[] as = cells; Cell a;
        //定義sum爲base值
        long sum = base;
        //如果cell數組不爲空0說明發生競爭
        if (as != null) {
            //遍歷數組裏的值進行累加
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        //如果cell數組爲空,說明沒有發生衝突競爭,直接返回base的值
        return sum;
    }

在實際運用的時候,只有從未出現過併發衝突的時候,base基數纔會使用到,一旦出現了併發衝突,之後所有的操作都只針對Cell[]數組中的單元Cell。

    /**
     * Adds the given value.
     *
     * @param x the value to add
     */
    public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
           //如果cells數據是null,沒有競爭,直接通過cas操作將base的值加上x
        if ((as = cells) != null || !casBase(b = base, b + x)) {
           //如果cells數組不是null,說明發生競爭,設置uncontended 爲ture,並將數據放入cells數組中
            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);
        }
    }

//該方法主要是用一個死循環對cells數組中的元素進行操作,當要更新的位置的元素爲空時插入新的cell元素,
//否則在該位置進行CAS的累加操作,如果CAS操作失敗並且數組大小沒有超過核數就擴容cells數組

    final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        int h;
        if ((h = getProbe()) == 0) {
            ThreadLocalRandom.current(); // force initialization
            h = getProbe();  //返回當前線程的threadLocalRandomProbe值
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        for (;;) {
            Cell[] as; Cell a; int n; long v;
            if ((as = cells) != null && (n = as.length) > 0) {
                if ((a = as[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) {       // cells數組中對應位置沒有數據則插入新對象
                        Cell r = new Cell(x);   
                        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)       // 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))))         //對該位置的cell元素進行累加
                    break;
                else if (n >= NCPU || cells != as)
                    collide = false;            // At max size or stale   //判斷數組大小是否大於核數
                else if (!collide)
                    collide = true;
                else if (cellsBusy == 0 && casCellsBusy()) {         //對cells數組進行擴容,直接擴容爲2倍
                    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);
            }
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {  //cellsBusy這裏是做爲一個自旋鎖來使用的
                boolean init = false;
                try {                           // 初始化cells數組大小爲2
                    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))))      //對base進行CAS操作
                break;                          // Fall back on using base
        }
    }

 

而LongAdder最終結果的求和,並沒有使用全局鎖,返回值不是絕對準確的,他只能保證最終一致性,因爲調用這個方法時還有其他線程可能正在進行計數累加,所以只能得到某個時刻的近似值,這也就是LongAdder並不能完全替代LongAtomic的原因之一。

而且從測試情況來看,線程數越多,併發操作數越大,LongAdder的優勢越大,線程數較小時,AtomicLong的性能還超過了LongAdder。

除了新引入LongAdder外,還有引入了它的三個兄弟類:LongAccumulatorDoubleAdderDoubleAccumulator

LongAccumulator是LongAdder的增強版。LongAdder只能針對數值的進行加減運算,而LongAccumulator提供了自定義的函數操作。

通過LongBinaryOperator,可以自定義對入參的任意操作,並返回結果(LongBinaryOperator接收2個long作爲參數,並返回1個long)。

LongAccumulator內部原理和LongAdder幾乎完全一樣。

DoubleAdder和DoubleAccumulator用於操作double原始類型。

1.4使用場景

LongAdder提供的API和AtomicLong比較接近,兩者都能以原子 的方式對long型變量進行增減。

但是AtomicLong提供的功能其實更豐富,尤其是addAndGet、decrementAndGet、compareAndSet這些方法。

addAndGet、decrementAndGet除了單純的做自增自減外,還可以立即獲取增減後的值,而LongAdder則需要做同步控制才能精確獲取增減後的值。如果業務需求需要精確的控制計數,做計數比較,AtomicLong也更合適。

另外,從空間方面考慮,LongAdder其實是一種“空間換時間”的思想,從這一點來講AtomicLong更適合。

總之,低併發、一般的業務場景下AtomicLong是足夠了。如果併發量很多,存在大量寫多讀少的情況,那LongAdder可能更合適。適合的纔是最好的,如果真出現了需要考慮到底用AtomicLong好還是LongAdder的業務場景,那麼這樣的討論是沒有意義的,因爲這種情況下要麼進行性能測試,以準確評估在當前業務場景下兩者的性能,要麼換個思路尋求其它解決方案。

測試code:


import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;

/**
 * 類說明:LongAdder,原子類以及同步鎖性能測試
 */
public class LongAdderDemo {
    private static final int MAX_THREADS = 150;
    private static final int TASK_COUNT = 300;
    private static final int TARGET_COUNT = 10000000;

    /*三個不同類型的long有關的變量*/
    private AtomicLong acount = new AtomicLong(0L);
    private LongAdder lacount = new LongAdder();
    private long count = 0;

    /*控制線程同時進行*/
    private static CountDownLatch cdlsync = new CountDownLatch(TASK_COUNT);
    private static CountDownLatch cdlatomic = new CountDownLatch(TASK_COUNT);
    private static CountDownLatch cdladdr = new CountDownLatch(TASK_COUNT);


    /*普通long的同步鎖測試方法*/
    protected synchronized long inc() {
        return ++count;
    }

    protected synchronized long getCount() {
        return count;
    }

    /*普通long的同步鎖測試任務*/
    public class SyncTask implements Runnable {
        protected String name;
        protected long starttime;
        LongAdderDemo out;

        public SyncTask(long starttime, LongAdderDemo out) {
            this.starttime = starttime;
            this.out = out;
        }

        @Override
        public void run() {
            long v = out.getCount();
            while (v < TARGET_COUNT) {
                v = out.inc();
            }
            long endtime = System.currentTimeMillis();
            System.out.println("SyncTask spend:" + (endtime - starttime) + "ms" );
            cdlsync.countDown();
        }
    }

    /*普通long的執行同步鎖測試*/
    public void testSync() throws InterruptedException {
        ExecutorService exe = Executors.newFixedThreadPool(MAX_THREADS);
        long starttime = System.currentTimeMillis();
        SyncTask sync = new SyncTask(starttime, this);
        for (int i = 0; i < TASK_COUNT; i++) {
            exe.submit(sync);
        }
        cdlsync.await();
        exe.shutdown();
    }

    /*原子型long的測試任務*/
    public class AtomicTask implements Runnable {
        protected String name;
        protected long starttime;

        public AtomicTask(long starttime) {
            this.starttime = starttime;
        }

        @Override
        public void run() {
            long v = acount.get();
            while (v < TARGET_COUNT) {
                v = acount.incrementAndGet();
            }
            long endtime = System.currentTimeMillis();
            System.out.println("AtomicTask spend:" + (endtime - starttime) + "ms" );
            cdlatomic.countDown();
        }
    }


    /*原子型long的執行測試*/
    public void testAtomic() throws InterruptedException {
        ExecutorService exe = Executors.newFixedThreadPool(MAX_THREADS);
        long starttime = System.currentTimeMillis();
        AtomicTask atomic = new AtomicTask(starttime);
        for (int i = 0; i < TASK_COUNT; i++) {
            exe.submit(atomic);
        }
        cdlatomic.await();
        exe.shutdown();
    }

    /*LongAdder的測試任務*/
    public class LongAdderTask implements Runnable {
        protected String name;
        protected long startTime;

        public LongAdderTask(long startTime) {
            this.startTime = startTime;
        }

        @Override
        public void run() {
            long v = lacount.sum();
            while (v < TARGET_COUNT) {
                lacount.increment();
                v = lacount.sum();
            }
            long endtime = System.currentTimeMillis();
            System.out.println("LongAdderTask spend:" + (endtime - startTime) + "ms");
            cdladdr.countDown();
        }

    }

    /*LongAdder的執行測試*/
    public void testLongAdder() throws InterruptedException {
        ExecutorService exe = Executors.newFixedThreadPool(MAX_THREADS);
        long startTime = System.currentTimeMillis();
        LongAdderTask longAdderTask = new LongAdderTask(startTime);
        for (int i = 0; i < TASK_COUNT; i++) {
            exe.submit(longAdderTask);
        }
        cdladdr.await();
        exe.shutdown();
    }

    public static void main(String[] args) throws InterruptedException {
        LongAdderDemo demo = new LongAdderDemo();
        demo.testSync();
        demo.testAtomic();
        demo.testLongAdder();
    }
}

二、StampLock

StampedLock是Java8引入的一種新的所機制,簡單的理解,可以認爲它是讀寫鎖的一個改進版本,讀寫鎖雖然分離了讀和寫的功能,使得讀與讀之間可以完全併發,但是讀和寫之間依然是衝突的,讀鎖會完全阻塞寫鎖,它使用的依然是悲觀的鎖策略.如果有大量的讀線程,他也有可能引起寫線程的飢餓。

而StampedLock則提供了一種樂觀的讀策略,這種樂觀策略的鎖非常類似於無鎖的操作,使得樂觀鎖完全不會阻塞寫線程。 

它的思想是讀寫鎖中讀不僅不阻塞讀,同時也不應該阻塞寫。

讀不阻塞寫的實現思路:

在讀的時候如果發生了寫,則應當重讀而不是在讀的時候直接阻塞寫!即讀寫之間不會阻塞對方,但是寫和寫之間還是阻塞的!

StampedLock的內部實現是基於CLH的。

code:

import java.util.concurrent.locks.StampedLock;

/**
 * 類說明:JDK1.8源碼自帶的示例
 */
public class StampedLockDemo {
    //一個點的x,y座標
    private double x,y;
    /**Stamped類似一個時間戳的作用,每次寫的時候對其+1來改變被操作對象的Stamped值
     * 這樣其它線程讀的時候發現目標對象的Stamped改變,則執行重讀*/
    private final StampedLock sl = new StampedLock();

    //【寫鎖(排它鎖)】
    void move(double deltaX,double deltaY) {// an exclusively locked method
        /**stampedLock調用writeLock和unlockWrite時候都會導致stampedLock的stamp值的變化
         * 即每次+1,直到加到最大值,然後從0重新開始*/
        long stamp =sl.writeLock(); //寫鎖
        try {
            x +=deltaX;
            y +=deltaY;
        } finally {
            sl.unlockWrite(stamp);//釋放寫鎖
        }
    }

    //【樂觀讀鎖】
    double distanceFromOrigin() { // A read-only method
        /**
         * tryOptimisticRead是一個樂觀的讀,使用這種鎖的讀不阻塞寫
         * 每次讀的時候得到一個當前的stamp值(類似時間戳的作用)
         */
        long stamp = sl.tryOptimisticRead();
        //這裏就是讀操作,讀取x和y,因爲讀取x時,y可能被寫了新的值,所以下面需要判斷
        double currentX = x, currentY = y;
        /**如果讀取的時候發生了寫,則stampedLock的stamp屬性值會變化,此時需要重讀,
         * validate():比較當前stamp和獲取樂觀鎖得到的stamp比較,不一致則失敗。
         * 再重讀的時候需要加讀鎖(並且重讀時使用的應當是悲觀的讀鎖,即阻塞寫的讀鎖)
         * 當然重讀的時候還可以使用tryOptimisticRead,此時需要結合循環了,即類似CAS方式
         * 讀鎖又重新返回一個stampe值*/
        if (!sl.validate(stamp)) {//如果驗證失敗(讀之前已發生寫)
            stamp = sl.readLock(); //悲觀讀鎖
            try {
                currentX = x;
                currentY = y;
            }finally{
                sl.unlockRead(stamp);//釋放讀鎖
            }
        }
        //讀鎖驗證成功後執行計算,即讀的時候沒有發生寫
        return Math.sqrt(currentX *currentX + currentY *currentY);
    }

    //讀鎖升級爲寫鎖
    void moveIfAtOrigin(double newX, double newY) { // upgrade
        // 讀鎖(這裏可用樂觀鎖替代)
        long stamp = sl.readLock();
        try {
            //循環,檢查當前狀態是否符合
            while (x == 0.0 && y == 0.0) {
                long ws = sl.tryConvertToWriteLock(stamp);
                //如果寫鎖成功
                if (ws != 0L) {
                    stamp = ws;// 替換stamp爲寫鎖戳
                    x = newX;//修改數據
                    y = newY;
                    break;
                }
                //轉換爲寫鎖失敗
                else {
                    //釋放讀鎖
                    sl.unlockRead(stamp);
                    //獲取寫鎖(必要情況下阻塞一直到獲取寫鎖成功)
                    stamp = sl.writeLock();
                }
            }
        } finally {
            //釋放鎖(可能是讀/寫鎖)
            sl.unlock(stamp);
        }
    }
}

三、CompleteableFuture

Future的不足

Future是Java 5添加的類,用來描述一個異步計算的結果。你可以使用isDone方法檢查計算是否完成,或者使用get阻塞住調用線程,直到計算完成返回結果,你也可以使用cancel方法停止任務的執行。

雖然Future以及相關使用方法提供了異步執行任務的能力,但是對於結果的獲取卻是很不方便,只能通過阻塞或者輪詢的方式得到任務的結果。阻塞的方式顯然和我們的異步編程的初衷相違背,輪詢的方式又會耗費無謂的CPU資源,而且也不能及時地得到計算結果,爲什麼不能用觀察者設計模式當計算結果完成及時通知監聽者呢?。

Java的一些框架,比如Netty,自己擴展了Java的 Future接口,提供了addListener等多個擴展方法,Google guava也提供了通用的擴展Future:ListenableFuture、SettableFuture 以及輔助類Futures等,方便異步編程。

同時Future接口很難直接表述多個Future 結果之間的依賴性。實際開發中,我們經常需要達成以下目的:

將兩個異步計算合併爲一個——這兩個異步計算之間相互獨立,同時第二個又依賴於第一個的結果。

等待 Future 集合中的所有任務都完成。

僅等待 Future集合中最快結束的任務完成(有可能因爲它們試圖通過不同的方式計算同一個值),並返回它的結果。

應對 Future 的完成事件(即當 Future 的完成事件發生時會收到通知,並能使用 Future 計算的結果進行下一步的操作,不只是簡單地阻塞等待操作的結果)

CompleteableFuture

JDK1.8才新加入的一個實現類CompletableFuture,實現了Future<T>, CompletionStage<T>兩個接口。實現了Future接口,意味着可以像以前一樣通過阻塞或者輪詢的方式獲得結果。

創建

除了直接new出一個CompletableFuture的實例,還可以通過工廠方法創建CompletableFuture的實例

工廠方法:

Asynsc表示異步,而supplyAsync與runAsync不同在與前者異步返回一個結果,後者是void.第二個函數第二個參數表示是用我們自己創建的線程池,否則採用默認的ForkJoinPool.commonPool()作爲它的線程池。

獲得結果的方法

public T get()

public T get(long timeout, TimeUnit unit)

public T getNow(T valueIfAbsent)

public T join()

getNow有點特殊,如果結果已經計算完則返回結果或者拋出異常,否則返回給定的valueIfAbsent值。

join返回計算的結果或者拋出一個unchecked異常(CompletionException),它和get對拋出的異常的處理有些細微的區別。

輔助方法

public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)

public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)

allOf方法是當所有的CompletableFuture都執行完後執行計算。

anyOf方法是當任意一個CompletableFuture執行完後就會執行計算,計算的結果相同。

CompletionStage是一個接口,從命名上看得知是一個完成的階段,它代表了一個特定的計算的階段,可以同步或者異步的被完成。你可以把它看成一個計算流水線上的一個單元,並最終會產生一個最終結果,這意味着幾個CompletionStage可以串聯起來,一個完成的階段可以觸發下一階段的執行,接着觸發下一次,再接着觸發下一次,……….。

總結CompletableFuture幾個關鍵點:

1、計算可以由 Future ,Consumer 或者 Runnable 接口中的 apply,accept 或者 run等方法表示。

2、計算的執行主要有以下

a. 默認執行

b. 使用默認的CompletionStage的異步執行提供者異步執行。這些方法名使用someActionAsync這種格式表示。

c. 使用 Executor 提供者異步執行。這些方法同樣也是someActionAsync這種格式,但是會增加一個Executor 參數。

CompletableFuture裏大約有五十種方法,但是可以進行歸類

變換類 thenApply:

關鍵入參是函數式接口Function。它的入參是上一個階段計算後的結果,返回值是經過轉化後結果。

消費類 thenAccept:

關鍵入參是函數式接口Consumer。它的入參是上一個階段計算後的結果, 沒有返回值。

 執行操作類 thenRun:

對上一步的計算結果不關心,執行下一個操作,入參是一個Runnable的實例,表示上一步完成後執行的操作。

結合轉化類:

需要上一步的處理返回值,並且other代表的CompletionStage 有返回值之後,利用這兩個返回值,進行轉換後返回指定類型的值。

兩個CompletionStage是並行執行的,它們之間並沒有先後依賴順序,other並不會等待先前的CompletableFuture執行完畢後再執行。

結合轉化類

對於Compose可以連接兩個CompletableFuture,其內部處理邏輯是當第一個CompletableFuture處理沒有完成時會合併成一個CompletableFuture,如果處理完成,第二個future會緊接上一個CompletableFuture進行處理。

第一個CompletableFuture 的處理結果是第二個future需要的輸入參數。

結合消費類:

需要上一步的處理返回值,並且other代表的CompletionStage 有返回值之後,利用這兩個返回值,進行消費

運行後執行類:

不關心這兩個CompletionStage的結果,只關心這兩個CompletionStage都執行完畢,之後再進行操作(Runnable)。

取最快轉換類:

兩個CompletionStage,誰計算的快,我就用那個CompletionStage的結果進行下一步的轉化操作。現實開發場景中,總會碰到有兩種渠道完成同一個事情,所以就可以調用這個方法,找一個最快的結果進行處理。

取最快消費類:

兩個CompletionStage,誰計算的快,我就用那個CompletionStage的結果進行下一步的消費操作。

取最快運行後執行類:

兩個CompletionStage,任何一個完成了都會執行下一步的操作(Runnable)。

異常補償類:

當運行時出現了異常,可以通過exceptionally進行補償。

運行後記錄結果類:

action執行完畢後它的結果返回原始的CompletableFuture的計算結果或者返回異常。所以不會對結果產生任何的作用。

運行後處理結果類:

運行完成時,對結果的處理。這裏的完成時有兩種情況,一種是正常執行,返回值。另外一種是遇到異常拋出造成程序的中斷。

 四、擴充知識點- Disruptor

應用背景和介紹

Disruptor是英國外匯交易公司LMAX開發的一個高性能隊列,研發的初衷是解決內部的內存隊列的延遲問題,而不是分佈式隊列。基於Disruptor開發的系統單線程能支撐每秒600萬訂單,2010年在QCon演講後,獲得了業界關注。

據目前資料顯示:應用Disruptor的知名項目有如下的一些:Storm, Camel, Log4j2,還有目前的美團點評技術團隊也有很多不少的應用,或者說有一些借鑑了它的設計機制。

Disruptor是一個高性能的線程間異步通信的框架,即在同一個JVM進程中的多線程間消息傳遞。

傳統隊列問題

在JDK中,Java內部的隊列BlockQueue的各種實現,仔細分析可以得知,隊列的底層數據結構一般分成三種:數組、鏈表和堆,堆這裏是爲了實現帶有優先級特性的隊列暫且不考慮。

在穩定性和性能要求特別高的系統中,爲了防止生產者速度過快,導致內存溢出,只能選擇有界隊列;同時,爲了減少Java的垃圾回收對系統性能的影響,會盡量選擇 Array格式的數據結構。這樣篩選下來,符合條件的隊列就只有ArrayBlockingQueue。但是ArrayBlockingQueue是通過加鎖的方式保證線程安全,而且ArrayBlockingQueue還存在僞共享問題,這兩個問題嚴重影響了性能。

ArrayBlockingQueue的這個僞共享問題存在於哪裏呢,分析下核心的部分源碼,其中最核心的三個成員變量爲

是在ArrayBlockingQueue的核心enqueue和dequeue方法中經常會用到的,這三個變量很容易放到同一個緩存行中,進而產生僞共享問題。

高性能的原理

引入環形的數組結構:數組元素不會被回收,避免頻繁的GC,

無鎖的設計:採用CAS無鎖方式,保證線程的安全性

屬性填充:通過添加額外的無用信息,避免僞共享問題

環形數組結構是整個Disruptor的核心所在。

首先因爲是數組,所以要比鏈表快,而且根據我們對上面緩存行的解釋知道,數組中的一個元素加載,相鄰的數組元素也是會被預加載的,因此在這樣的結構中,cpu無需時不時去主存加載數組中的下一個元素。而且,你可以爲數組預先分配內存,使得數組對象一直存在(除非程序終止)。這就意味着不需要花大量的時間用於垃圾回收。此外,不像鏈表那樣,需要爲每一個添加到其上面的對象創造節點對象—對應的,當刪除節點時,需要執行相應的內存清理操作。環形數組中的元素採用覆蓋方式,避免了jvm的GC。

其次結構作爲環形,數組的大小爲2的n次方,這樣元素定位可以通過位運算效率會更高,這個跟一致性哈希中的環形策略有點像。在disruptor中,這個牛逼的環形結構就是RingBuffer,既然是數組,那麼就有大小,而且這個大小必須是2的n次方

其實質只是一個普通的數組,只是當放置數據填充滿隊列(即到達2^n-1位置)之後,再填充數據,就會從0開始,覆蓋之前的數據,於是就相當於一個環。

每個生產者首先通過CAS競爭獲取可以寫的空間,然後再進行慢慢往裏放數據,如果正好這個時候消費者要消費數據,那麼每個消費者都需要獲取最大可消費的下標。

同時,Disruptor 不像傳統的隊列,分爲一個隊頭指針和一個隊尾指針,而是隻有一個角標(上圖的seq),它屬於一個volatile變量,同時也是我們能夠不用鎖操作就能實現Disruptor的原因之一,而且通過緩存行補充,避免僞共享問題。該指針是通過一直自增的方式來獲取下一個可寫或者可讀數據。

 

本章主要講了LongAdder,StampLock,CompleteableFuture等java8中新增的一些特性,以及簡單介紹了Disruptor,希望大家在開發中可以有更多的選擇。

其他閱讀   

併發編程專題十-其他的併發容器

併發編程專題九-併發容器ConcurrentHashMap源碼分析

併發編程專題八-hashMap死循環分析

併發編程專題七-什麼是線程安全

併發編程專題六-線程池的使用與原理

併發編程專題五-AbstractQueuedSynchronizer源碼分析

併發編程專題四-原子操作和顯示鎖

併發編程專題三-JAVA線程的併發工具類

併發編程專題二-線程間的共享和協作

併發編程專題一-線程相關基礎概念

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