java併發編程(十九)Semaphore、CountdownLatch和CyclicBarrier你都玩過嗎? 一、簡介 二、使用案例 三、原理

在JUC這個線程同步工具包下,有幾個比較遊戲的類,Semaphore、CountdownLatch和CyclicBarrier,你都用過嗎?下面我們就來簡單介紹下他們的用法,並且提供些簡單的代碼示例,方便大家理解。

一、簡介

  • Semaphore:通常翻譯成信號量,用來控制共享變量可以同時被線程訪問的數量。

    通過構造方法指定計數,線程使用acquire()方法獲取許可,當達到執行計數後,其他線程將不能再次獲取,並進入阻塞,知道獲取許可的線程執行1release()1釋放許可。

  • CountdownLatch:常被稱作門栓, 用來進行線程的同步協作,等待所有線程到達後,在執行後續操作。

    通過構造方法指定線程數量,主線程使用await()進行等待線程到達,工作線程使用countDown()進行報到,也就是讓計數減一。

  • CyclicBarrier:常被稱爲柵欄,用來進行線程的同步協作,等待達到預設的計數,在執行後續操作。

    通過構造方法指定計數,線程使用await()方法進行同步等待,當線程等待數達到計數值時繼續執行。

二、使用案例

2.1 Semaphore

共十個線程,設置兩個信號量:

public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2);
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println("線程" + Thread.currentThread().getName() + "佔用時間:" + LocalDateTime.now());
                    Thread.sleep(2000);
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

結果如下,每兩個線程的執行時間是相同的,即每次只允許兩個線程同時執行:

線程Thread-2佔用時間:2022-02-16T09:56:25.589
線程Thread-1佔用時間:2022-02-16T09:56:25.589
線程Thread-0佔用時間:2022-02-16T09:56:27.593
線程Thread-4佔用時間:2022-02-16T09:56:27.593
線程Thread-5佔用時間:2022-02-16T09:56:29.595
線程Thread-3佔用時間:2022-02-16T09:56:29.595
線程Thread-8佔用時間:2022-02-16T09:56:31.603
線程Thread-9佔用時間:2022-02-16T09:56:31.603
線程Thread-6佔用時間:2022-02-16T09:56:33.615
線程Thread-7佔用時間:2022-02-16T09:56:33.615

2.2 CountdownLatch

設置數值爲10,10個線程,只有當10個線程全部到達後,主線程纔會繼續執行:

    public static void main(String[] args) throws InterruptedException {
        // 使用倒計數門閂器 ,迫使主線程進入等待 ;設置門栓的值爲10
        CountDownLatch latch = new CountDownLatch(10);
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                //門栓值減1
                latch.countDown();
                System.out.println("當前門栓值:" + latch.getCount());
            }
        }).start();
        //阻塞主線程,等門栓值爲0,主線程執行
        latch.await();
        System.out.println("主線程執行。。。");
    }

結果如下:

當前門栓值:9
當前門栓值:8
當前門栓值:7
當前門栓值:6
當前門栓值:5
當前門栓值:4
當前門栓值:3
當前門栓值:2
當前門栓值:1
當前門栓值:0
主線程執行。。。

2.3 CyclicBarrier

設置計數值爲6,1個主線程,5個工作線程,當6個線程全部到達後,纔會繼續執行:

    public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(6);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + "準備就緒");
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread().getName() + "到達");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        Thread.sleep(1000);
        System.out.println(Thread.currentThread().getName() + "準備開始");
        cyclicBarrier.await();
    }

結果:

Thread-0準備就緒
Thread-2準備就緒
Thread-3準備就緒
Thread-1準備就緒
Thread-4準備就緒
main準備開始
Thread-2到達
Thread-3到達
Thread-0到達
Thread-1到達
Thread-4到達

三、原理

3.1 Semaphore

首先看下類圖:

僅僅包含我們常見的三個內部類:Sync,FairSync,NonfairSync。Sync是AQS的子類。

3.1.1 構造方法

直接看最底層,我們設置的計數被設置成state:

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

3.1.2 acquire()方法

下面簡單分析其源碼,首先來看acquire()方法:

獲取AQS當中的可中斷共享鎖:

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

AQS方法:

    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

嘗試獲取共享鎖tryAcquireShared(),前面學習中提到過,默認是非公平鎖:

    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);
        }
    }

真正獲取鎖的邏輯:

        final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                // 獲取狀態
                int available = getState();
                int remaining = available - acquires;
                // 如果小於0,獲取失敗, 進入 doAcquireSharedInterruptibly
                if (remaining < 0 ||
                    // 如果cas成功,返回正數,表示成功
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

doAcquireSharedInterruptibly方法:

    private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
        // 添加到等待隊列
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (; ; ) {
                // 獲取前面的節點
                final Node p = node.predecessor();
                if (p == head) {
                    // 如果是頭結點,嘗試獲取許可
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        // 成功
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                // 不成功, 設置上一個節點 阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

3.1.3 release()方法

走的是Sync的釋放共享鎖方法

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

AQS的方法:

    public final boolean releaseShared(int arg) {
        // 嘗試釋放鎖,Semaphore的方法
        if (tryReleaseShared(arg)) {
            // 釋放,AQS的方法,處理隊列和狀態相關內容
            doReleaseShared();
            return true;
        }
        return false;
    }

Semaphore自己實現的方法,循環,知道成功爲止:

        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");
                // 使用cas
                if (compareAndSetState(current, next))
                    return true;
            }
        }

3.2 CountdownLatch

類圖如下:

只有一個內部類Sync。Sync繼承自AQS。

3.2.1 構造方法:

直接點進去看最後面,如下:

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

state被設置爲我們指定的值。

3.2.2 await() 方法

同樣使用的是Sync的方法:

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

    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

不同之處是其自己實現的tryAcquireShared方法:

        protected int tryAcquireShared(int acquires) {
            // 構造設置的值肯定是大於0,此處一定是-1,所以會阻塞
            return (getState() == 0) ? 1 : -1;
        }

3.2.3 countDown()方法

也是通過AQS的方法如下:

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

直接看其自己實現的tryReleaseShared方法,

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

3.3 CyclicBarrier

3.3.1 構造方法

直接看底層構造,parties 就是我們設置的線程數量,初始化時count與parties 相等:

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

3.3.2 await()方法

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

看看其dowait()方法,很長,包含各種策略,主要看中文註釋的重點位置就行了,建議寫代碼跟蹤一下:

    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        // 使用的ReetrantLock
        final ReentrantLock lock = this.lock;
        // 上鎖,防止多線程造成併發問題
        lock.lock();
        try {
            final Generation g = generation;

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

            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }
            // 總線程數-1
            int index = --count;
            if (index == 0) {  // tripped
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    // 喚醒所有等待中的線程
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }

            // 循環,直到觸發、中斷、中斷或超時
            for (;;) {
                try {
                    // 默認是false
                    if (!timed)
                        // conditiont條件隊列 的await()
                        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();
        }
    }

當所有的index減少爲0時,會走次方法nextGeneration(),此方法主要的作用就是更新柵欄狀態,並且喚醒所有等待的線程。

    private void nextGeneration() {
        // signal completion of last generation
        trip.signalAll();
        // set up next generation
        count = parties;
        generation = new Generation();
    }

上面的流程有幾個點:

  • 上鎖lock
  • 減去計數index
  • 如果index爲0,就執行nextGeneration喚醒所有等待的線程,並重置狀態。
  • 釋放鎖unlock

3.4 簡單總結

  • Semaphore、CountdownLatch和CyclicBarrier都是在JUC下的,用於線程同步的工具類。
  • Semaphore、CountdownLatch的核心還是AQS,而CyclicBarrier則不是。
  • Semaphore、CountdownLatch的狀態修改都是基於CAS(比較並替換),而CyclicBarrier使用了ReentrantLock。
  • 雖說CyclicBarrier沒有直接使用AQS的子類,但是其使用的ReentrantLock仍然是通過AQS實現的。
  • AQS是JUC下的核心。

關於上面的三個類,就簡單介紹完了,我們在工作當中其實很容易記混,希望本文可以給你帶來一點幫助,讓你能夠在項目當中正確的使用它們。

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