一看就会的CountDownLatch源码分析

126 阅读5分钟

#CountDownLatch

1.先说明CountDownLatch的最通俗的理解,常用的两个方法是:

countDown()与await()方法 一个线程等待多个线程,多个线程执行完毕后,该线程才会向下执行。这么说的话着实有点抽象,那么就举例同时结合源码进行说明。


public static void main(String[] args) throws InterruptedException {
    CountDownLatch countDownLatch = new CountDownLatch(2);

    for (int i = 1; i <= 2; i++) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " come in!!!");
            countDownLatch.countDown();
        }, String.valueOf(i)).start();
    }
    countDownLatch.await();  // 等待计数器归零,再向下执行,这里是主线程在等待
    System.out.println("close door!!!");
}

首先利用构造器传入的参数为2,之后会说明参数2的含义,不急先向下看,模拟了两个人进屋的场景,而且在其内部调用了countDown()方法,下面又调用了await()方法,当计数器减为0时,主线程才会向下执行,否则主线程会等待。

##countDown()方法的原理

从方法的调用去说明:

CountDownLatch.jpg 这样看来是不是就比较简单了,方法的调用好少啊!!! 那么现在开始上源码: 上方的例子中调用后进入该方法体内:

public void countDown() {
    sync.releaseShared(1);  // 由于Sync继承了AQS,即调用AQS中的方法
}

继续:

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

tryReleaseShared()在Sync类中,重写了AQS的方法(本身该方法就是留给子类重写的)

protected boolean tryReleaseShared(int releases) {
    for (;;) {  // 死循环
        int c = getState();  // 这里获取状态变量,就是例子中提到的传入的参数即是赋值给AQS中的一个state变量
        if (c == 0)  // 若为0
            return false;
        int nextc = c-1;  // 获取状态值减1
        if (compareAndSetState(c, nextc))  // 利用CAS修改状态变量的值
            return nextc == 0;
    }
}

当releaseShared(int arg)方法中if条件成立时,表示当前的变量state已经减为0,就需要唤醒调用await()方法在等待的线程,而如何唤醒该线程,关键点就在doReleaseShared()方法中。

接着看:

// 以下就要结合await()方法才能说明
private void doReleaseShared() {
    for (;;) {  // 死循环
        Node h = head;  // h指向头结点,
        if (h != null && h != tail) {  // 条件成立至少有两个节点  
            int ws = h.waitStatus;  // 获取等待状态值
            if (ws == Node.SIGNAL) {  // 若状态为SIGNAL状态则执行
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))  // 利用CAS设置waitStatus为0,设置失败,不唤醒后继节点
                    continue;            // loop to recheck cases
                unparkSuccessor(h);  // 唤醒后继节点
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}
private void unparkSuccessor(Node node) {  // 唤醒后继节点
    int ws = node.waitStatus;  //
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)  // s节点不为空,唤醒后继节点(s节点其中包含了线程的信息)
        LockSupport.unpark(s.thread);
}

再说明例子中调用await()方法,main()线程等待,是如何被唤醒的。该源码和我之前分析的ReentrantLock(可重入锁)中的await()方法大相径庭。juejin.cn/post/703732… 那就再说明下吧!!!

await()源码如下:

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)  // 判断当前status变量是否为0
        doAcquireSharedInterruptibly(arg);  
}
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;  // 为0返回1,否则返回-1
}
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);  // 当线程被唤醒时,执行此句时,r>=0条件成立,则会设置头结点,清除头结点的信息
                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);
    }
}

doAcquireSharedInterruptibly()方法在计数器未减为0时,阻塞调用该方法的线程,当计数器减为0时,该线程被唤醒,那么如何被唤醒,就要看此方法doReleaseShared()中调用了unparkSuccessor(h),进行唤醒。

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

这应该很明显了,例子中只有一个等待的main线程,当计数器减为0时,即会唤醒头结点h的后继节点,首先,判断ws是否小于0,若小于0,则调用compareAndSetWaitStatus(node, ws, 0),设置WaitStatus为0,开始时,头结点的ws是为0的,条件不成立,则向下执行,获取后继节点s,再重申下,此时例子中只有一个main线程在执行,因此以下的if条件不成立,即为:node.next不为空,直接调用LockSupport.unpark()方法唤醒main线程。 可能这时候就会有人想,不仅仅只有一个线程在等待呢?那又是怎样的呢?不慌听我慢慢道来。 假设存在两个及以上的线程是如何运行的呢? 那就分析两个线程的情况(多个线程和两个线程实际是类似的就不多说了,两个线程会了,多个线程也就明白了!!!) 关键点在doAcquireSharedInterruptibly(int arg)方法中,其中调用了setHeadAndPropagate(node, r)方法具体实现如下:

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);  // 清除头结点的信息
    
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();  // 其他的线程都会在此处被唤醒
    }
}

多个线程是被唤醒时流程如下:首先当计数器减为0时,会唤醒一个线程,调用了unparkSuccessor(h)方法,接着await方法处的某个线程被唤醒,在doAcquireSharedInterruptibly()方法中的以下语句会被执行,进入setHeadAndPropagate(node, r)方法中。

int r = tryAcquireShared(arg);
if (r >= 0) {
    setHeadAndPropagate(node, r);
    p.next = null; // help GC
    failed = false;
    return;
}

setHeadAndPropagate(node, r)方法如下:

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);

    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();  // 被唤醒的线程会执行此句
    }
}

doReleaseShared()源码如下:

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);  // 被唤醒的线程又会唤醒下一个线程,如此往复直到所有的线程被唤醒为止,在无异常发生的情况下
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

实际关键是在计数器减为0时,会唤醒某个线程,接着该线程会唤醒另一个线程,另一个线程又会唤醒另一个线程,这样直到所有的线程被唤醒。

通常情况下,假如只有一个线程,调用await()方法等待的线程是就像是导游,调用countDown()方法的线程就像是驴友,导游只有等驴友们都到齐了之后,才能出发。当然也可以设置等待超时的时间,等一段时间,人没到齐,导游也不管了,直接出发。 以下的代码是封装含有线程信息的节点,构造为一个双端对列。

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);  // 将当前线程封装为节点
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail; 
    // 两个if若成立,则是形成了一个双端队列,尾结点为新封装的节点
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);  // 将节点入队
    return node;
}
private Node enq(final Node node) {
    for (;;) {  // 死循环
        Node t = tail;  
        if (t == null) {  // 尾节点为空,利用CAS则设置头结点
            if (compareAndSetHead(new Node()))
                tail = head;  
        } else {  // 存在尾节点,利用尾插法将node节点插入到尾部
            node.prev = t;  
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;  // 返回前驱节点
            }
        }
    }
}