12.CountDownLatch源码分析

122 阅读4分钟

源码分析

构造函数

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}
//首先从构造函数出发 初始化状态变量 ,其中sync是一个AQS的子类,构造函数如下
//设置状态变量state,其中state是个volatile 用于保证可见性 此时state为5
Sync(int count) {
    setState(count);
}

await

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    //首先判断是否被中断,中断就抛出异常,否则的的话与tryAcquireShared(arg)的返回值相比较
    if (Thread.interrupted())        
        throw new InterruptedException();
    //判断state是否为0 如果是0 就返回1 否则返回-1
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

state的值代表待达到条件的线程数,比如初始化为5,表示待达到条件的线程数为5,每次调用countDown()函数都会减1,所以当没有达到条件也就是state不等于0将会返回-1,进入if语句 ,接着往下看 这方法看起来很长,当然首先你要明白,线程进入await方法阻塞后,会用一个一个的节点将线程串起来。等达到条件后再一个一个的唤醒,该链表是一个双向链表。

//判断State是否已经为0 此时state为5不为0 进入  doAcquireSharedInterruptibly(arg);
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}
private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    //该函数 用于将当前线程相关的节点将入链表尾部
    //注意节点的类型是Node.SHARED
    final Node node = addWaiter(Node.SHARED); 
    boolean failed = true;
    try {
    //将入无限for循环
        for (;;) {  
             //获得它的前节点
            final Node p = node.predecessor(); 
            if (p == head) {
                //判断 state是不是0  
                //非0 返回r=-1 说明要阻塞
                //是0 返回r= 1 说明要放开门栓传播
                int r = tryAcquireShared(arg);
                //唯一的退出条件,也就是await()方法返回的条件很重要!!
                //当 state为0 时 r=1 会进入
                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

让我们首先看看shouldParkAfterFailedAcquire(p, node)方法 首先明白p是node的前节点

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus; //获取前节点的状态
    if (ws == Node.SIGNAL) //表明前节点可以运行
​
        return true;
    if (ws > 0) { //如果前节点状态大于0表明已经中断即取消
        do {
            node.prev = pred = pred.prev; //链表的基本操作,相信你可以看懂
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL); //等于0进入这里
    }
    //只有前节点状态为NodeSIGNAL才返回真
    return false; 
}
 

parkAndCheckInterrupt

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

我们对shouldParkAfterFailedAcquire来进行一个整体的概述,首先应该明白节点的状态,节点的状态是为了表明当前线程的良好度,如果当前线程被打断了,在唤醒的过程中是不是应该忽略该线程,这个状态标志就是用来做这个的具体有如下几种

/** waitStatus value to indicate thread has cancelled */ 线程已经被取消
static final int CANCELLED =  1;
/** waitStatus value to indicate successor's thread needs unparking */ 线程需要去被唤醒
static final int SIGNAL    = -1;
/** waitStatus value to indicate thread is waiting on condition */ 线程正在唤醒等待条件
static final int CONDITION = -2;
/**
 * waitStatus value to indicate the next acquireShared should //线程的共享锁应该被无条件传播
 * unconditionally propagate
 */
static final int PROPAGATE = -3;
​

目前你只需知道大于0时表明该线程已近被取消,已是无效节点,不应该被唤醒,注意:初始化链头节点时头节点状态值为0。

shouldParkAfterFailedAcquire是位于无限for循环内的,这一点需要注意一般每个节点都会经历两次循环后然后被阻塞。当该函数返回true时 线程调用parkAndCheckInterrupt这个进入park阻塞自身。到这里基本每个调用await函数都阻塞在这里 (很关键哦,因为下次唤醒,从这里开始执行哦)

setHeadAndPropagate

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node); //这里重新设置头节点 (已上面  第一次释放锁 h== head 的重复判断相对应)
    if (propagate > 0 || h == null || h.waitStatus < 0) {
        Node s = node.next;
        //注意Node.SHARED会进入继续释放
        if (s == null || s.isShared())
            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;       //明白这里为什么要加一次判断了吧!!!,被唤醒的线程会在执行该函数
    }
}
​

现在明白其唤醒机制了吧 先唤醒一个线程(第一个阻塞的线程) 然后被唤醒的线程又会执行到这里唤醒线程,如此重复下去 最终所有线程都会被唤醒,其实这也是AQS共享锁的唤醒原理,自此完成了对countDownLatch阻塞和唤醒原理的基本分析。

countDown

public void countDown() {
    sync.releaseShared(1);
}
//该函数也是委托其内部类完成,具体实现如下 这里arg为1 哦
public final boolean releaseShared(int arg) {
    //state 为零时才返回真
    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;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;  //nextc 为零时才返回真
    }
}
//也就是说当state减1后为0时才会返回为真 执行后面的唤醒条件,否则都一概忽略,假设达到唤醒条件 具体来看如何唤醒 ,函数如下
private void doReleaseShared() {
for (;;) {
        Node h = head; //获取头节点,
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) { //头结点的状态为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
        }
        // loop if head changed
        if (h == head)                   
        //这里是否有疑问明明都有这个 Node h = head为啥还要在判断一次?多次一举别着急后面有原因
            break;  
    }
}

unparkSuccessor

private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    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); //唤醒线程
}
​
/***
对于这个操作很好理解的,首先取该节点的后节点就行唤醒,如果后节点已被取消,则从最后一个开始往前找,找一个满足添加的节点进行唤醒
​
有人肯能会有疑问,要是如果有多个节点只在这进行一次唤醒工作吗?难道只唤醒一个线程就可以了?哈哈别急还记得线程是在哪阻塞的吗 让我们回来前面去看线程被阻塞的地方 (忘记了可以往前看看)
​
这一小段
***/
​
setHeadAndPropagate(node, r); //关键就在这个函数哦
p.next = null; // help GC
failed = false;
return;

setHeadAndPropagate

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node); //这里重新设置头节点 (已上面  第一次释放锁 h== head 的重复判断相对应)
    if (propagate > 0 || h == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            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;       //明白这里为什么要加一次判断了吧!!!,被唤醒的线程会在执行该函数
    }
}
​
//现在明白其唤醒机制了吧 先唤醒一个线程(第一个阻塞的线程) 
//然后被唤醒的线程又会执行到这里唤醒线程,如此重复下去 
//最终所有线程都会被唤醒,其实这也是AQS共享锁的唤醒原理,自此完成了对countDownLatch阻塞和唤醒原理的基本分析

总结

1.通过CountDownLatch(int count)构造器给sync同步器的state赋值。
​
2.每调await方法一次都会去判断state是不是为0,如果是0方法结束,如果不为0时 入队并park,直到被park唤醒。
//死循环
for (;;) {
                //如果状态==0说明 countdown已经执行了state次
                if (state == 0) {
                    //获取前置节点
                    //因为第一次被唤醒的是第一个节点
                    //第一个节点的头结点一定是头
                    final Node p = node.predecessor();
                    if (p == head) {
                        //将自己设置为头结点
                        //继续唤醒自己的下一个节点
                       setHeadAndPropagate(node, r);
                    }
                }
                //节点1阻塞在这里
                //被唤醒后继续循环
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
​
3.每调countDown一次state数值-1,直到state为0时调用 unparkSuccessor(h),唤醒等待队列中head后的第一个线程。
​
private void unparkSuccessor(Node node) {
        //头结点的下一个节点 即 第一个节点
        Node s = node.next;
        //唤醒节点1
        if (s != null)
            LockSupport.unpark(s.thread);
    }
​
4.这个线程被唤醒后首先调用setHeadAndPropagate将自己设置为头结点---》doReleaseShared---》unparkSuccessor(h)继续唤醒其他await的方法
​