CountDownLatch、CyclicBarrier和Semaphore幾個併發容器的使用

CountDownLatch、CyclicBarrier和Semaphore幾個併發容器的使用

在講這幾個容器之前,本人講述一個小面試題,該題的描述:讓A、B、C三個線程同時執行,並且依次輸出A、B、C三個字母十次。面試時,想法方向是對的,但是結果是錯的。我想到的是兩種實現,分別是:wait、notifyAll配合使用和併發容器的使用。

CountDownLatch

該容器主要的作用是:多個線程之間,主線程等待其他線程執行完畢開始往下執行。代碼如下:

public static void main(String[] args) throws InterruptedException {
        CountDownLatch downLatch = new CountDownLatch(3);
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName());
            downLatch.countDown();
        }).start();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName());
            downLatch.countDown();
        }).start();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName());
            downLatch.countDown();
        }).start();
        // 等待前面三個線程執行完畢
        downLatch.await();
        System.out.println(Thread.currentThread().getName());
    }

打印的結果如下:

Thread-0
Thread-2
Thread-1
main

CyclicBarrier

該容器的作用是:所有線程會在某個時間等待其他線程都準備完畢纔開始執行。代碼如下:

public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(3);
        new Thread(() -> {
            System.out.println("===== " + Thread.currentThread().getName() + " Start");
            try {
                barrier.await();
                System.out.println("===== " + Thread.currentThread().getName() + " Run");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "A").start();
        new Thread(() -> {
            System.out.println("===== " + Thread.currentThread().getName() + " Start");
            try {
                barrier.await();
                System.out.println("===== " + Thread.currentThread().getName() + " Run");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "B").start();
        new Thread(() -> {
            System.out.println("===== " + Thread.currentThread().getName() + " Start");
            try {
                barrier.await();
                System.out.println("===== " + Thread.currentThread().getName() + " Run");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "C").start();
    }

結果如下:

===== A Start
===== C Start
===== C Run
===== A Run
===== B Run

統一執行完start就等待其他線程到start完畢後開始一起執行run

Semaphore

該容器的作用是: 限制同時執行的線程數量,所以可以用於限流作用;代碼如下:

public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(1);
        CyclicBarrier barrier = new CyclicBarrier(3);
        new Thread(() -> {
            try {
                System.out.println(" ===" + Thread.currentThread().getName());
                barrier.await();
                // 獲取令牌
                semaphore.acquire();
                System.out.println(" ===" + Thread.currentThread().getName() + " Run");
                Thread.sleep(3000);
                // 返還令牌
                semaphore.release();
            } catch (Exception e) {
                e.printStackTrace();
            }

        }).start();

        new Thread(() -> {
            try {
                System.out.println(" ===" + Thread.currentThread().getName());
                barrier.await();
                // 獲取令牌
                semaphore.acquire();
                System.out.println(" ===" + Thread.currentThread().getName() + " Run");
                Thread.sleep(3000);
                // 返還令牌
                semaphore.release();
            } catch (Exception e) {
                e.printStackTrace();
            }

        }).start();

        new Thread(() -> {
            try {
                System.out.println(" ===" + Thread.currentThread().getName());
                barrier.await();
                // 獲取令牌
                semaphore.acquire();
                System.out.println(" ===" + Thread.currentThread().getName() + " Run");
                Thread.sleep(3000);
                // 返還令牌
                semaphore.release();
            } catch (Exception e) {
                e.printStackTrace();
            }

        }).start();
    }

結果如下:

 ===Thread-0
 ===Thread-1
 ===Thread-2
 ===Thread-2 Run
 ===Thread-1 Run
 ===Thread-0 Run

2 Run 、1 Run、0 Run 之間會有間隔,間隔時間爲3S,等待獲取執行權的線程歸還令牌;如果去掉semaphore.acquire();semaphore.release();這兩句,三個Run之間瞬間執行完畢

最後我們再次回到開始的面試題,這題有兩個關鍵點是同時執行和循序打印ABC三個字母;考察的是對多線程之間,線程控制的掌握程度。

同時執行 我們可以使用wait 和 notifyAll配合使用解決 或 CyclicBarrier容器解決;方法2上面代碼示例代碼已經給出;1方法代碼如下:

	
	private static volatile int flag = 0;
    private static final Object object = new Object();
    private static void test() throws InterruptedException {
        new Thread(() -> {
            try {
                synchronized (object) {
                    flag++;
                    object.wait();
                    System.out.println(" = A = ");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            try {
                synchronized (object) {
                    flag++;
                    object.wait();
                    System.out.println(" = B = ");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            try {
                synchronized (object) {
                    flag++;
                    object.wait();
                    System.out.println(" = C = ");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        // 三個線程都啓動完畢
        while (flag < 3) {
        }
        System.out.println(" ============= ");
        Thread.sleep(5000);
        synchronized (object) {
            object.notifyAll();
        }
    }

效果:打印完=============暫停了5秒,然後打印ABC

依次循環打印ABC

  1. 普通方法是通過定義一個可見變量分別取餘判定打印,代碼如下:

        //TODO 可見性 或者使用 int 類型 爲什麼?
    	// 好像是基本類型變量存儲在棧中,然後呢?求解
    	private static volatile Integer ctr = 1;
    
        private static void test2() {
            new Thread(() -> print(), "A").start();
            new Thread(() -> print(), "B").start();
            new Thread(() -> print(), "C").start();
        }
    
        private static void print() {
            int i = 1;
            String name = Thread.currentThread().getName();
            char ch = name.charAt(0);
            while (i <= 10) {
                if ((ctr % 3) == (ch + 1 - 'A') || ((ctr % 3 == 0) && ch == 'C')) {
                    System.out.println("i = " + i + "--" + ch);
                    i++;
                    ctr++;
                }
            }
        }
    

    效果如下:

    i = 1--A
    i = 1--B
    i = 1--C
    i = 2--A
    i = 2--B
    i = 2--C
    i = 3--A
    i = 3--B
    i = 3--C
    i = 4--A
    i = 4--B
    i = 4--C
    ....
    
  2. 用Semaphore容器,思路是定義三個該容器,兩兩交叉互相釋放(返還)對方的令牌;代碼如下:

    	private static Semaphore sepA = new Semaphore(1);
        private static Semaphore sepB = new Semaphore(0);
        private static Semaphore sepC = new Semaphore(0);
        private static void test2() {
            // A線程
            new Thread(() -> {
                try {
                    for (int i = 0; i < 10; i++) {
                        // 取A的令牌
                        sepA.acquire();
                        System.out.println("i = " + (i+1) + "--A");
                        // 返還B的令牌
                        sepB.release();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
    		
            // B線程
            new Thread(() -> {
                try {
                    for (int i = 0; i < 10; i++) {
                        // 獲取B的令牌,A執行完纔有B的令牌
                        sepB.acquire();
                        System.out.println("i = " + (i+1) + "--B");
                        // 釋放C的令牌
                        sepC.release();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
    
            new Thread(() -> {
                try {
                    for (int i = 0; i < 10; i++) {
                        // 獲取C的令牌,B執行完纔有C的令牌
                        sepC.acquire();
                        System.out.println("i = " + (i+1) + "--C");
                        // 釋放A的令牌,進入下一次循環
                        sepA.release();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    

    效果如下:

    i = 1--A
    i = 1--B
    i = 1--C
    i = 2--A
    i = 2--B
    i = 2--C
    i = 3--A
    i = 3--B
    i = 3--C
    i = 4--A
    i = 4--B
    i = 4--C
    ....
    

CountDownLatch源碼分析

刪掉註釋,發現該類的代碼很少,如下:


package java.util.concurrent;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class CountDownLatch {
    /**
     * Synchronization control For CountDownLatch.
     * Uses AQS state to represent count.
     * 用於CountDownLatch的同步控制,使用AQS狀態表示計數
     */
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            setState(count);
        }

        int getCount() {
            return getState();
        }

        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

    private final Sync sync;

    /**
     * CountDownLatch 的代碼如下
     */
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

    public void countDown() {
        sync.releaseShared(1);
    }
    
    public long getCount() {
        return sync.getCount();
    }
}

發現CountDownLatch的代碼沒啥看的,只是包裝了一個API供調用,在這裏很疑惑爲什麼不直接暴露Sync而要再次包裝一層 ,希望有人解答一下。

所以我們看一下Sync類做了什麼,第一眼看到的是繼承了AbstractQueuedSynchronizer類,該類就是同步容器AQS。所以說CountDownLatch的其實是基於AQS。對於AQS鑑於篇幅過長,暫不講述。

CyclicBarrier源碼分析

我們看一下幾個重要的方法代碼

字段和構造方法

    /** The lock for guarding barrier entry */
    private final ReentrantLock lock = new ReentrantLock();
    /** Condition to wait on until tripped */
    private final Condition trip = lock.newCondition();

	/** The number of parties */
    private final int parties;
    /* The command to run when tripped */
    private final Runnable barrierCommand;
	/**
     * Number of parties still waiting. Counts down from parties to 0
     * on each generation.  It is reset to parties on each new
     * generation or when broken.
     */
    private int count;
	public CyclicBarrier(int parties) {
        this(parties, null);
    }    

	public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }

parties 等待線程羣組數,不知道這個翻譯對不對

barrierCommand

count

構造方法沒什麼說的,初始化配置參數

await方法

代碼如下:

    public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }

    public int await(long timeout, TimeUnit unit)
        throws InterruptedException,
               BrokenBarrierException,
               TimeoutException {
        return dowait(true, unit.toNanos(timeout));
    }

    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            final Generation g = generation;
			// 默認是false 如果爲true說明打破屏障了,線程已經運行了
            if (g.broken)
                throw new BrokenBarrierException();

            if (Thread.interrupted()) {
                // 當前線程(用戶線程)是否被打斷
                breakBarrier();
                throw new InterruptedException();
            }
			
            // 每調用一次改方法,count 自減1
            int index = --count;
            if (index == 0) {  
                // tripped 如果count 爲0 線程開始執行
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    // 喚醒所有的等待線程並且重置容器配置
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        // 如果上面喚醒失敗 打破屏障,喚醒所有線程
                        breakBarrier();
                }
            }

            // loop until tripped, broken, interrupted, or timed out
            // 一直循環等待
            for (;;) {
                try {
                    if (!timed)
                        // 沒有超時設置 當前線程等待
                        trip.await();
                    else if (nanos > 0L)
                        // 沒有超時設置 當前線程時效等待
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        // We're about to finish waiting even if we had not
                        // been interrupted, so this interrupt is deemed to
                        // "belong" to subsequent execution.
                        Thread.currentThread().interrupt();
                    }
                }

                if (g.broken)
                    throw new BrokenBarrierException();

                if (g != generation)
                    return index;

                if (timed && nanos <= 0L) {
                    // 等待超時 打破屏障喚醒所有線程
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }
	
	/**
	* 打破屏障,singnal所有線程,運行
	*/
    private void breakBarrier() {
        generation.broken = true;
        count = parties;
        trip.signalAll();
    }

兩個public方法都最終調用的是private方法,具體解釋看代碼片段註釋。從源碼中我們可以發現CyclicBarrier的實現原理是基於ReentrantLockCondition 的實現,這兩玩意是什麼,自行百度。其實和 對象的awaitnotify/notifyAll 方法類似的功能,而ReentrantLockCondition 又有一部分是基於 AQS實現的。此時是不是會發現AQS在併發控制和多線程的地位很高,有時間該去分析一下它的源碼.

Semaphore的源碼淺析

內部類

    // 又是基於AQS的實現
	abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 1192457210091910933L;

        Sync(int permits) {
            setState(permits);
        }

        final int getPermits() {
            return getState();
        }
		
        // 非公平的獲取許可認證
        final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 || // 就因爲沒有剩餘判斷就非公平,爲啥?求解釋
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }
		
        // 釋放許可認證
        protected final boolean tryReleaseShared(int releases) {
            for (;;) {
                int current = getState();
                int next = current + releases;
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                if (compareAndSetState(current, next))
                    return true;
            }
        }
		
        // 減少許可認證
        final void reducePermits(int reductions) {
            for (;;) {
                int current = getState();
                int next = current - reductions;
                if (next > current) // underflow
                    throw new Error("Permit count underflow");
                if (compareAndSetState(current, next))
                    return;
            }
        }
		
        // 清空許可認證
        final int drainPermits() {
            for (;;) {
                int current = getState();
                if (current == 0 || compareAndSetState(current, 0))
                    return current;
            }
        }
    }

    /**
     * NonFair version
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -2694183684443567898L;

        NonfairSync(int permits) {
            super(permits);
        }

        protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }
    }

    /**
     * Fair version
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = 2014338818796000944L;

        FairSync(int permits) {
            super(permits);
        }

        protected int tryAcquireShared(int acquires) {
            for (;;) {
                if (hasQueuedPredecessors())
                    return -1;
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }
    }

發現這些內部類又是基於AQS的實現。

構造方法

    public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }

    public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }

構造方法比較簡單,就兩個。通過構造方法發現默認是非公平實現,要公平實現一定要構造方法中指明。

acquire和release方法

    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    public void acquire(int permits) throws InterruptedException {
        if (permits < 0) throw new IllegalArgumentException();
        sync.acquireSharedInterruptibly(permits);
    }

    public void acquireUninterruptibly() {
        sync.acquireShared(1);
    }

    public void acquireUninterruptibly(int permits) {
        if (permits < 0) throw new IllegalArgumentException();
        sync.acquireShared(permits);
    }

    public boolean tryAcquire() {
        return sync.nonfairTryAcquireShared(1) >= 0;
    }

    public boolean tryAcquire(int permits) {
        if (permits < 0) throw new IllegalArgumentException();
        return sync.nonfairTryAcquireShared(permits) >= 0;
    }

    public boolean tryAcquire(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }


    public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
        throws InterruptedException {
        if (permits < 0) throw new IllegalArgumentException();
        return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
    }

    public void release() {
        sync.releaseShared(1);
    }

    public void release(int permits) {
        if (permits < 0) throw new IllegalArgumentException();
        sync.releaseShared(permits);
    }

每一個acquirerelease都有兩個,一個是默認獲取/釋放一個令牌和獲取/釋放自定義數量令牌。

總結

最後我們發現其實我們可以基與ReentrantLockCondition再實現等待一起執行,代碼如下:

    public static void main(String[] args) throws InterruptedException {
        thread();
        System.out.println("before sleep");
        Thread.sleep(20000);
        System.out.println("after sleep");
        thread();
    }

    private static ReentrantLock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();
    private static volatile Integer count = 2;

    private static void thread() {
        new Thread(() -> {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + " Start");
                if (count > 1) {
                    count--;
                    condition.await();
                } else {
                    condition.signalAll();
                }
                System.out.println(Thread.currentThread().getName() + " Run");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

        }).start();
    }

效果如下:

before sleep
Thread-0 Start
after sleep
Thread-1 Start
Thread-1 Run
Thread-0 Run

線程0 會等待 線程1 Start 之後開始一起執行

發現AQS在多線程和併發控制地位非常重要,離不開它的身影。

本人沒有仔細觀看AQS的源碼,所以沒有解釋和AQS相關的方法功能以及作用,後面有時間看了AQS源碼後再來補全吧。還有用詞不準確的地方希望大家指出。只是通過這麼一個面試題,學習了一下線程之間控制的方法方式。

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