JUC并发工具--CountDownLatch的使用和原理解析

CountDownLatch 概念

CountDownLatch可以使一个获多个线程等待其他线程各自执行完毕后再执行

CountDownLatch 定义了一个计数器,和一个阻塞队列, 当计数器的值递减为0之前,阻塞队列里面的线程处于挂起状态,当计数器递减到0时会唤醒阻塞队列所有线程,这里的计数器是一个标志,可以表示一个任务一个线程,也可以表示一个倒计时器,CountDownLatch可以解决那些一个或者多个线程在执行之前必须依赖于某些必要的前提业务先执行的场景。

CountDownLatch 常用方法说明

//构造方法,创建一个值为count 的计数器
CountDownLatch(int count);

//阻塞当前线程,将当前线程加入阻塞队列
await();

//在timeout的时间之内阻塞当前线程,时间一过则当前线程可以执行
await(long timeout, TimeUnit unit);

//对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程
countDown();

CountDownLatch实现原理

1、创建计数器

// 当我们 new CountDownLatch(4) 的时候
CountDownLatch countDownLatch = new CountDownLatch(4);

// 此时会创建一个AQS的同步队列
// 并把创建CountDownLatch 传进来的计数器赋值给AQS队列的 state
// 所以state的值也代表CountDownLatch所剩余的计数次数
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);//创建同步队列,并设置初始计数器值
}

2、阻塞线程

当我们调用countDownLatch.wait()的时候
1. 会创建一个节点,加入到AQS阻塞队列
2. 并同时把当前线程挂起
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

1. 判断计数器是否计数完毕,未完毕则把当前线程加入AQS阻塞队列
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted()) {
        throw new InterruptedException();
    }
    // 锁重入次数大于0 则新建节点加入阻塞队列,挂起当前线程
    if (tryAcquireShared(arg) < 0) {
        doAcquireSharedInterruptibly(arg);
    }
}

2. 构建阻塞队列的双向链表,挂起当前线程
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
    //新建节点加入阻塞队列
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            //获得当前节点pre节点
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);//返回锁的state
                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、计数器递减

当我们调用countDownLatch.countDown()方法的时候
会对计数器进行减1操作
AQS内部是通过释放锁的方式,对state进行减1操作
当state=0的时候证明计数器已经递减完毕
此时会将AQS阻塞队列里的节点线程全部唤醒

public void countDown() {
    //递减锁重入次数,当state=0时唤醒所有阻塞线程
    sync.releaseShared(1);
}

1. 对计数器进行减1操作
public final boolean releaseShared(int arg) {
    // 递减锁的重入次数,如果当前 state == 0,直接返回
    // 计数器 -1,使用cas方式进行递减,判断最新的值是不是 0
    if (tryReleaseShared(arg)) {
        //唤醒队列所有阻塞的节点
        doReleaseShared();
        return true;
    }
    return false;
}

2. 唤醒所有阻塞队列里面的线程
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;
                }
                //成功则唤醒线程
                unparkSuccessor(h);
            } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
                continue;
            }
        }
        if (h == head)
            break;
    }
}

参考:

https://zhuanlan.zhihu.com/p/95835099

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