關於java多線程淺析七:CountDownLatch的原理分析和使用

什麼是CountDownLatch

CountDownLatch與CyclicBarrier一樣,也是一個用與同步的輔助類,它的使用場景是:在一個或者一組其他線程沒有執行完畢之前,使當前線程進行等待,只有其他的線程全部完成執行完畢後,當前線程才能繼續進行。前一篇我們介紹了CyclicBarrier,在這裏說一下CountDownLatch與CyclicBarrier的區別。

  • CyclicBarrier是執行了CyclicBarrier.await( )方法的幾個線程之間互相等待,當所有的其他線程都執行了await方法時(除了主線程),所有線程繼續執行;而CountDownLatch是一個線程等待一個或者多個其他線程都執行完畢,並且調用了CountDownLatch.countDown( )方法後,才能繼續執行。

  • CountDownLatch和CyclicBarrier都有一個計數器的概念,但是CyclicBarrier的計數器可以重置繼續使用(有一個 Generation 的概念,可以參考上一篇博客);而CountDownLatch則沒有這個概念,計數器不能被重置,所以不能重複使用。

CountDownLatch使用示例

下面通過一個示例來了解一下CountDownLatch的使用。

/**
 * Created by fei on 2017/6/9.
 */
public class CountDownLatchDemo {


    public static final int INIT_SIZE = 3;
    private static CountDownLatch countDownLatch;

    public static void main(String[] args) {
        try {
            System.out.println("我想結婚");
            countDownLatch = new CountDownLatch(INIT_SIZE);

            new ThreadDemo("求婚成功").start();
            new ThreadDemo("雙方父母點頭了").start();
            new ThreadDemo("房子也買好了").start();

            countDownLatch.await();
            Thread.sleep(1000);
            System.out.println("好了,可以去領證了");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class ThreadDemo extends Thread {

        public ThreadDemo(String name) {
            super(name);
        }

        @Override
        public void run() {

            try {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName());
                countDownLatch.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

哈哈,舉了一個小例子,正常操作下,只有這幾個條件都滿足了,也就是這幾個線程(求婚成功、雙方父母點頭了、房子也買好了)都執行完了纔可能去執行領證操作。

我想結婚
求婚成功
房子也買好了
雙方父母點頭了
好了,可以去領證了

從示例的代碼上可以看出來,這個CountDownLatch的使用和CyclicBarrier有異曲同工之妙。具體的,我們再放張圖片來加強理解:

這裏寫圖片描述

圖中的示例和代碼表達的意思是一樣的,就是利用CountDownLatch來實現不同線程之間的協同。其中,這個 cnt 變量,也就是代碼中的 INIT_SIZE int類型變量是關鍵,它代表的意思就是執行了CountDownLatch.await( )方法的線程需要另外的線程執行幾次 CountDownLatch.countDown( )方法。下面,就根據具體方法的源碼來看一下CountDownLatch是怎樣實現的。

CountDownLatch源碼解析

CountDownLatch的源碼不是很長,我就將去掉註釋的全部源碼展示在這裏:


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

public class CountDownLatch {
    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;

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

    public String toString() {
        return super.toString() + "[Count = " + sync.getCount() + "]";
    }
}

可以看出,CountDownLatch的源碼不是很難,我們從主要的await方法和countDown方法講起。

await方法

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

可以看出,await實際上調用的是AQS的acquireSharedInterruptibly(1)方法,acquireSharedInterruptibly()方法如下:

    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) {
            return (getState() == 0) ? 1 : -1;
        }

getState() = 0 就是表示此時的鎖是沒有被佔用的,是可以獲取的狀態。 否則,執行doAcquireSharedInterruptibly( )方法:

    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //創建包裹當前線程的節點,初始化爲“共享鎖”,將Node節點添加到AQS維護的隊列中
        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);
        }
    }

shouldParkAfterFailedAcquire方法

// 根據名字就能猜到這個方法的意思
//獲取鎖失敗後當前結點(線程)是否應該足阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 獲取前一個節點的狀態
    int ws = pred.waitStatus;
    // 如果前一個節點是SIGNAL狀態,就意味這當前線程應該被unpark喚醒。
    //並且這裏返回true。
    if (ws == Node.SIGNAL)
        return true;
    // 如果前一個節點的狀態 > 0 
    //(這裏代表的是線程已取消狀態,有關Node節點,將在下一個章節中講解)
    if (ws > 0) {
       //向前尋找,一直找到不爲取消狀態的節點(節點pred)
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;//把當前結點設置爲節點pred的後繼節點
    } else {
        // 如果前一個節點 < 0,則設置前繼節點爲SIGNAL狀態。
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

可能看到這裏會對waitStatus的狀態有一些不理解,再詳細講解Node節點之前,我們先說一下,這裏的waitStatus都有哪些狀態。

//下面是AQS類中,Node節點的源碼片段,針對的是waitStatus的狀態值

    // 線程已被取消
    static final int CANCELLED =  1;
    // 當前結點(線程)被釋放掉或者取消掉時,它的後繼節點(線程)需要被喚醒
    // 這個SIGNAL就可以理解成“我要通知下一個節點可以被喚醒了”
    static final int SIGNAL    = -1;
    // 線程在等待Condition喚醒
    static final int CONDITION = -2;
    // 其它線程獲取到“共享鎖”
    static final int PROPAGATE = -3;
    // 值得注意的是,當 waitStatus=0時,意味着當前線程不屬於上面的任何一種狀態。
    volatile int waitStatus;

看完Node中關於節點狀態的的解釋,上面的代碼應該容易理解多了。

parkAndCheckInterrupt方法

和shouldParkAfterFailedAcquire方法一樣,parkAndCheckInterrupt也是定義在AQS類中的方法。

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

這個方法很簡單,就是調用LockSupport的park方法,將當前線程進行阻塞(順便提一嘴,JUC包中的線程的阻塞和喚醒,都是調用的LockSupport這個類),然後返回線程的中斷狀態。

countDown方法

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

countDown方法調用的是sync的releaseShared()方法:

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

可以看出,releaseShared方法先是調用tryReleaseShared方法獲取鎖,如果獲取失敗,則調用doReleaseShared方法再進行獲取鎖操作。(大神不愧是大神!其實doReleaseShared方法中調用的還是tryReleaseShared方法,爲啥要這樣寫?讓我寫可能就直接硬生生的去做獲取鎖操作了)
這裏的tryReleaseShared被CountDownLatch覆蓋了:

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;
        // 調用CAS函數進行賦值。
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

這個方法也很好理解,就是針對 state 進行減操作。

源碼分析總結

其實看完countDown方法就應該可以看出CountDownLatch具體的設計與使用思路了,就是使用CountDownLatch的時候,先進行”鎖計數器”(也就是 INIT_SIZE 參數,或者更簡單的說是CountDownLatch這個類的“Count”),這個數值很關鍵,它決定了調用CountDownLatch.await()方法的線程想要繼續運行下去,則需要多少次調用CountDownLatch.countDown()方法。當調countDown方法時,鎖計數器減 1 ,知道所計數器爲 0 時,執行CountDownLatch.await()的線程才能獲取到鎖,從而繼續執行。

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