# 前言
CountDownLatch 是一种减法计数器,可以让一个或多个线程等待,常应用于 m 个线程需要等待其他 n 个线程执行完某个逻辑,才能继续执行的场景。
# 使用示例
# 源码分析
# sync
同步器中,唯一的变量就是 sync,定义了同步器的同步操作。Sync 继承了 AQS,主要依赖共享锁的相关方法,实现了 tryAcquireShared 和 tryReleaseShared 方法。
# 构造函数
Sync(int count) { | |
//state 初始化为 count,然后进行递减操作 | |
setState(count); | |
} |
# tryAcquireShared
protected int tryAcquireShared(int acquires) { | |
//state 为 0 时可以直接获取到锁 | |
return (getState() == 0) ? 1 : -1; | |
} |
这里为啥 state 为 0 时可以直接获取到锁呢?想一下 CountDownLatch 的使用场景,某些线程需要依赖其他 n 个线程的执行,state 初始为 n,只有这 n 个线程全部释放锁,即将 state 减为 0 时,才表示这 n 个线程执行完毕,从而依赖这 n 个线程的线程不用等待了,可以继续执行了。
# tryReleaseShared
protected boolean tryReleaseShared(int releases) { | |
// 自旋 | |
for (;;) { | |
int c = getState(); | |
// 状态是 0,说明没有要释放的锁了,已经被前 n 个线程释放了 | |
if (c == 0) | |
return false; | |
// 状态不是 0,进行减 1 操作 | |
int nextc = c-1; | |
if (compareAndSetState(c, nextc)) | |
// 返回是否完全释放了锁 | |
return nextc == 0; | |
} | |
} |
# 构造函数
首先是同步器的构造,构造函数需要指定计数器的数量。
public CountDownLatch(int count) { | |
if (count < 0) throw new IllegalArgumentException("count < 0"); | |
// 初始 state 为 count | |
this.sync = new Sync(count); | |
} |
# 主要方法
# await
public void await() throws InterruptedException { | |
// 直接调用 AQS 方法进行阻塞等待,直到中断或者锁已释放 | |
sync.acquireSharedInterruptibly(1); | |
} | |
public boolean await(long timeout, TimeUnit unit) | |
throws InterruptedException { | |
// 带超时的 | |
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)); | |
} |
# countDown
public void countDown() { | |
// 直接调用 AQS 方法进行锁的释放 | |
sync.releaseShared(1); | |
} |
# 场景说明
前提条件:线程 a 和线程 b 依赖线程 1 和线程 2 的执行。
# ① 初始化一个 count 为 2 的 CountDownLatch
同步器的 state 为 2。
# ② 线程 a 等待
线程 a 调用 await () 进行等待,此时 state 为 2,尝试获取锁失败。然后加入等待队列,等待队列需要初始化,初始化后 head 和 tail 指向同一个 waitStatus 为 0 的空节点。初始化完毕,新节点加入等待队列,tail 变为指向新节点。此时新节点的前置节点是 head,所以再次尝试下能不能获取到锁,不能的话才进行后续的等待。再次尝试获取锁失败,将 head 节点的 waitStatus 设置为 -1,然后新节点进入等待状态。
# ③ 线程 1 执行完毕
线程 1 调用 countDown () 进行释放锁,尝试释放锁时成功将 state 从 2 更新为 1,但未完全释放锁,尝试释放共享锁失败,此时不会进行锁的释放。
# ④ 线程 2 执行完毕,线程 a 继续执行
线程 2 调用 countDown () 将 state 更新为 0,尝试释放为成功。head 的 waitStatus 为 -1,说明有等待的线程需要唤醒。将 head 的 waitStatus 设置为 0,然后唤醒队列中等待的线程。线程 a 从上次等待的位置继续执行,尝试获取锁成功,将当前节点设置为 head,并继续唤醒后面的线程。因为队列中没有其他等待的线程了,所以此次 await 结束,线程 a 继续执行后面的代码。
# ⑤ 线程 b
线程 b 调用 await () 直接返回,因为此时 state 为 0,直接获取到了锁。
# 总结
CountDownLatch 的逻辑其实很简单,只是对重写了尝试获取锁和尝试释放锁的逻辑,定义 state 什么情况下可以直接获取锁,什么情况下可以直接释放锁,其他的加入等待队列并等待唤醒的操作,直接交给 AQS 就好了。