一、概念
- CountDownLatch可以使一个线程等待指定数量的其他线程都执行完毕后再执行。
- CountDownLatch 是基于共享锁实现的,初始的时候声明一个state数量的同步器,这个state可以理解为拥有锁的线程数量,当调用await()方法时,当前线程会去尝试获取共享锁,只有当state值为0时才能获取锁成功,否则会阻塞,才能继续执行下面代码。
- 通俗理解,声明一个指定线程数量的计数器,通过countDown()方法计数减一,只有在数值建为0时,才会继续执行后面代码。
二、示例
public static void main(String[] args) throws InterruptedException {
test1();
// test();
}
public static void test1() throws InterruptedException {
int countNum = 5;
CountDownLatch latch = new CountDownLatch(countNum);
for (int i = 0; i < countNum; i++) {
new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "," + latch.getCount());
latch.countDown();
// latch.countDown();
// latch.countDown();
}).start();
}
latch.await();
System.out.println("全部执行完成");
}
三、源码解析
以下部分情况都会以这个例子为说明。
构造器,设置一个基于共享锁实现的同步器,初始state为count
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
// 设置一个基于共享锁实现的同步器,初始state为count
this.sync = new Sync(count);
}
内部类Sync实现了AQS的共享锁所需的方法tryAcquireShared和tryReleaseShared。tryAcquireShared为尝试获取共享锁的条件,这里自定义的条件为当前state值为0时获取成功。tryReleaseShared方法会将state值减1,当state值为0时,返回成功。
protected int tryAcquireShared(int acquires) {
// 共享锁的获取锁实现,当state值为0时获取成功,否则获取失败
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// 共享锁的释放锁实现,state值减1,当state为0时,表示锁释放成功
for (;;) {
int c = getState();
if (c == 0)
// 如果当前state值为0,则表示当前锁已经释放
return false;
int nextc = c-1;
// CAS设置减1后的state值
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
CountDownLatch类主要的两个方法之一的await()方法。以中断模式去尝试获取锁 — 即阻塞线程直到state值为0。countDown()方法会将state值减1,如果state值为0,则唤醒调用await()方法阻塞的线程。
public void await() throws InterruptedException {
// 以中断模式去尝试获取锁 --- 即阻塞线程直到state值为0,然后唤醒等待队列中的线程,
sync.acquireSharedInterruptibly(1);
}
// 将state值减一,如果state值为0,则唤醒等待队列中的线程。
public void countDown() {
sync.releaseShared(1);
}
await()方法中主要逻辑都在方法doAcquireSharedInterruptibly中。
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 将当前线程(举例中的main线程)以共享模式加入等待队列中
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
// 取当前节点的前继节点 如果是头结点 则尝试获取锁 (举例中main线程为头结点的后继节点)
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
// 如果获取锁成功,则将当前线程设置为头节点 -- 即state值已减为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);
}
}
addWaiter方法负责将当前线程加入队列中,如果队列为空,则初始化一个节点为头结点,将当前线程设为尾节点。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 先尝试快速插入队列中
Node pred = tail;
// 队列不为空 (举例中当前队列为空)
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) { // Must initialize
// 如果队列为空,则初始化队列,设置头结点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
// 将当前节点设置为尾结点
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
再回到doAcquireSharedInterruptibly方法中,第一次调用该await()方法时,此时当前线程(main线程)不是头结点,而是尾节点,所以执行下面的条件。
// 获取锁失败后 判断是否需要挂起 如果需要挂起,则执行挂起操作 被唤醒后继续执行循环体操作
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
shouldParkAfterFailedAcquire方法判断当前线程是否需要挂起。当当前线程的前继节点的状态为SIGNAL时,则表示当前线程应该被挂起。如果前继节点状态为CANCELLED 时,则将前继节点从队列中移出。如果需要挂起则调用parkAndCheckInterrupt()方法(UNSAFE方法)挂起当前线程。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 判断当前线程需不需要挂起, 如果一个线程节点的waitStatus为Node.SIGNAL 则表示它的后继节点需要被挂起或唤醒
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
// 线程状态为 CANCELLED 取消,则将该节点从队列中移出
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
// 如果waitStatus是0或者PROPAGATE 则表示需要被挂起,但不是现在挂起 所以设置状态为SIGNAL 下一次循环就可以确认为需要挂起
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
// 挂起当前线程 调用UNSAFE 的park方法
LockSupport.park(this);
return Thread.interrupted();
}
当其他线程都执行完成后,即state值减为0后,会唤醒队列中的线程。当这里挂起的线程被唤醒后,则会继续执行当前循环体。这里条件满足,会将当前线程设置为头结点。
setHeadAndPropagate方法将当前节点设置为头结点,并列举了需要唤醒下一个节点的条件。
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;
// 如果下一个节点为null,或者下一个节点时共享模式等待的则唤醒下一个节点
if (s == null || s.isShared())
// 释放共享锁, 正确步骤,首先是最后一个线程执行countDownLatch方法后,state值为0
// 会调用该方法,唤醒线程,此时头节点为new Node(), main线程为下一个节点,所以唤醒的是main线程
// 当main线程被唤醒后,会继续执行doAcquireSharedInterruptibly方法中循环体,
// 并将main线程设置为头节点(即调用当前方法setHeadAndPropagate)
// 当前方法中又会继续唤醒下一个节点 唤醒流程
doReleaseShared();
}
doReleaseShared方法,CountDownLatch整个流程中有两个调用该方法的地方,一个是countDown()方法是state为0时,另一个是在await()方法阻塞后被唤醒时调用的setHeadAndPropagate方法里面的。
唤醒正确流程为:
- 当最后一个线程执行countDown()方法后,state值减为0,调用doReleaseShared()方法唤醒线程,此时头节点为new Node(), main线程为下一个节点,所以唤醒的是main线程
- 当main线程被唤醒后,会继续执行doAcquireSharedInterruptibly方法中循环体,并将main线程设置为头节点(即调用当前方法setHeadAndPropagate)
- 当前方法中满足条件后又会继续唤醒下一个节点
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// 取出头节点,如果头结点的状态为SIGNAL(下一个节点可以被唤醒)
// (举例中这里的头结点为new Node()) 最开始waitStatus为0 在判断是否需要挂起时将waitStatus改为了SIGNAL
if (ws == Node.SIGNAL) {
// 将头结点状态设置为可用
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 唤醒头结点的后继节点 (举例中的main线程) 如果等待队列中有多个节点?
// 1. 这里唤醒一个线程之后,在doAcquireInterruptibly(int arg)方法中 被阻塞的线程唤醒了 继续执行循环体,并且设置为了头结点
// 2. 这里继续执行执行循环体,直到队列所有可以唤醒的线程都被唤醒
// 3. 当都被唤醒后,不会再去唤醒,则不会去更改头结点
// 4. if (h == head) 条件满足,释放成功
unparkSuccessor(h);
}
// 如果状态为0(可用) 则设置为PROPAGATE
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) {
/*
* 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);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
// 后继节点 (举例中的main线程)
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);
}
以上纯属个人理解,如有理解错的地方,还请指出问题。