CyclicBarrier自旋改造

前言

最近java的concurrent庫學習的熱火朝天,有以 悲觀鎖 爲代表,AQS運用的如火純青的LinkedBlockingQueue,ArrayBlockingQueue。也有 樂觀鎖 爲代表,把 CAS+自旋 演示的出神入化的ConcurrentLinkedQueue。當然還有像ReentrantReadWriteLock,它的 獨佔鎖+共享鎖 實際運用,是教科書典範。

總之一句話,Doug Lea是佩服的五體投地。

最近開始學習CyclicBarrier,學着學着就感覺機會來了,CyclicBarrier主要用了ReentrantLock實現。被ReentrantLock鎖住的代碼一次只能由一個線程進入,我們完全可以改成 自旋+CAS 的方案,讓線程儘可能少的掛起,改造成一個無鎖版CyclicBarrier。

CyclicBarrier

CyclicBarrier 是什麼? 例如五個人(線程)要開會,但是要等到五個人都到了才行,而且開完會後可以重新等待五個人,循環往復的過程,難理解的話網上demo一堆,可以看看。

改造前,我們先看看CyclicBarrier的代碼,CyclicBarrier有這幾個重要的變量需要先了解:

  • parties: 同一批線程的個數,構造器就決定好了。

  • Generation: Generation描述着CyclicBarrier的更新換代。在CyclicBarrier中,同一批線程屬於同一代。當有parties個線程到達barrier,generation就會被更新換代。是決定線程到底是等待掛起還是喚醒返回的重要依據。

  • ReentrantLock and Condition: 線程掛起喚醒的重要工具,不必多說。

private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        //lock鎖上線程
        lock.lock();
        try {
        	//分代標記
            final Generation g = generation;

            ......
			//一個線程到了,總數-1
            int index = --count;
            // 如果這一批線程都到了,可以觸發一個Runnable任務
            if (index == 0) {  
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    //喚醒所有等待線程,並更新generation
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }

            //如果不是這一批倒數第一個線程,都先掛起再說。
            for (;;) {
            .....
                 trip.await();       
				//被喚醒後比對是不是改朝換代了(generation變了),如果是,你就可以返回了。
                if (g != generation)
                    return index;

                ....
            }
        } finally {
            //解鎖
            lock.unlock();
        }
    }

nextGeneration方法也很重要,倒數第一個線程喚醒的操作就在這裏了。

    private void nextGeneration() {
        // 喚醒這一批所有線程,大家都在等你,你肯定要叫醒大家
        trip.signalAll();
        // 改朝換代了,count要重新賦值,generation要重新賦值個對象。
        count = parties;
        generation = new Generation();
    }

如何改造

之前看過ConcurrentLinkedQueue的源碼,可以總結一下 自旋+CAS 的編碼特點:

  • 每一次自旋,cas代碼可以保證原子操作(只有一個線程能成功更改共享變量),保證操作準確性,但如果有兩個cas操作,就不能一前一後 順序執行,因爲這不是原子操作,在多線程環境下不保證操作準確性。
  • 拋棄鎖思想,樹立每行代碼之間都不存在前後能夠關聯,數據能夠依賴的思想(因爲很可能就被其它線程改了), 儘可能多的考慮多線程情況,多一些if else判斷可能的衝突情況,如果一次自旋週期內發現 走不到cas代碼,或者感覺可能會出現衝突,就需要continue繼續自旋,直到走到cas代碼執行成功。

下面就是我的無鎖版CyclicBarrier

import java.lang.reflect.Field;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.LockSupport;

/**
 * @program: 
 * @description:
 * @author: qian.pan
 * @create: 2019/07/11 17:09
 **/
public class CyclicBarrierNoLock {

    private final ConcurrentLinkedQueue<Thread> concurrentLinkedQueue;
    private final int parties;
    private final sun.misc.Unsafe UNSAFE;
    private final long countOffset;
    private volatile int count;
    private volatile Object generation;
    private final Runnable barrierCommand;

    public CyclicBarrierNoLock(int count) throws NoSuchFieldException, IllegalAccessException {
        this(count, null);
    }

    public CyclicBarrierNoLock(int count, Runnable barrierCommand) throws NoSuchFieldException, IllegalAccessException {
        this.count = parties = count;
        this.concurrentLinkedQueue = new ConcurrentLinkedQueue();
        generation = new Object();

        //獲取 Unsafe 內部的私有的實例化單例對象
        Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
        //無視權限
        field.setAccessible(true);
        UNSAFE = (sun.misc.Unsafe) field.get(null);
        Class<?> k = CyclicBarrierNoLock.class;
        countOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("count"));
        this.barrierCommand = barrierCommand;
    }

    public int await() {
        int index;
        head:
        while (true) {
            final Object g = generation;
            //防止count扣成負數
            if (count < 0) {
                continue head;
            }
			//cas更新不成功,重新自旋
            if (!casDecrement()) {
                continue head;
            }

            index = count;
            //cas操作,如果 count 能從0 重新設置成 parties,說明這一批線程已經到位,執行command命令,generation更新,然後喚醒睡眠線程。
            if (resetCount()) {
                final Runnable command = barrierCommand;
                if (command != null)
                    command.run();
                generation = new Object();
                signalAll();
                return 0;
            }


            while (true) {
			 //加入掛起隊列,掛起線程。
             concurrentLinkedQueue.offer(Thread.currentThread());
                LockSupport.park();
                //發現更新換代,返回
                if (g != generation) {
                    return index;
                }
            }
        }
    }

    private void signalAll() {
        while (!concurrentLinkedQueue.isEmpty()) {
            LockSupport.unpark(concurrentLinkedQueue.poll());
        }
    }

    private final boolean casDecrement() {
        return UNSAFE.compareAndSwapInt(this, countOffset, count, count - 1);
    }

    private final boolean resetCount() {
        return UNSAFE.compareAndSwapInt(this, countOffset, 0, parties);
    }

}

上面代碼看起來不像純自旋無阻塞,因爲還是利用了ConcurrentLinkedQueue來掛起暫時不用的線程,但是如果是純自旋等待的話 會無謂消耗cpu資源,我們只需要砍掉一些因爲ReentrantLock引起的阻塞豈可。

測試

我們貼上測試代碼測試我們的無鎖CyclicBarrierNoLock能否正常使用

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CyclicBarrierTest {
    private static CyclicBarrierNoLock cyclicBarrier;

    public static void main(String[] args) {
        int num = 5;
        try {
            cyclicBarrier = new CyclicBarrierNoLock(num, () -> log.info("人到齊了"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

        for (int i = 0; i < num; i++) {
            loop(num);
        }
    }

    private static void loop(int num) {
        for (int i = 0; i < num; i++) {
            Thread thread = new Thread(() -> {
                try {
                    Thread.sleep((long) (Math.random() * 2000));
                } catch (InterruptedException e) {
                }
                int index = cyclicBarrier.await();
                log.info(Thread.currentThread().getId() + ":" + index + ":我來了");
            });
            thread.setDaemon(false);
            thread.start();
        }
    }

}

測試了下沒什麼問題,至於爲什麼會先報 “人到齊了”,後纔有各線程的"我來了",是因爲代碼是先執行Command命令,後喚醒各掛起線程,CyclicBarrier代碼也一樣,試了下用CyclicBarrier測試也是這種輸出,不影響結果。

17:34:18.261 [Thread-19] INFO CyclicBarrierTest - 人到齊了
17:34:18.265 [Thread-16] INFO CyclicBarrierTest - 27:4:我來了
17:34:18.265 [Thread-3] INFO CyclicBarrierTest - 14:3:我來了
17:34:18.265 [Thread-23] INFO CyclicBarrierTest - 34:2:我來了
17:34:18.265 [Thread-4] INFO CyclicBarrierTest - 15:1:我來了
17:34:18.265 [Thread-19] INFO CyclicBarrierTest - 30:0:我來了
17:34:19.040 [Thread-21] INFO CyclicBarrierTest - 人到齊了
17:34:19.040 [Thread-1] INFO CyclicBarrierTest - 12:4:我來了
17:34:19.040 [Thread-12] INFO CyclicBarrierTest - 23:3:我來了
17:34:19.041 [Thread-6] INFO CyclicBarrierTest - 17:2:我來了
17:34:19.042 [Thread-9] INFO CyclicBarrierTest - 20:1:我來了
17:34:19.042 [Thread-21] INFO CyclicBarrierTest - 32:0:我來了
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章