什么是 CountDownLatch?
CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程执行完后再执行
示例
在具体讲解 CountDownLatch 之前我们先来举个例子:
比如我们在高中上晚自习的时候,对吧
快下晚自习的时候班主任就会过来说:等全部同学离开教室的时候之后班长负责关一下门
班长:收到
然后我们的程序是这样子的
package juc;
import java.util.concurrent.CountDownLatch;
/**
* @author Woo_home
* @create by 2020/3/13
*/
public class CountDownLatchDemo {
// 定义一个静态方法
public static void closeDoor() {
// 模拟 8 个同学
for (int i = 1; i <= 8; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "号同学离开教室");
},String.valueOf(i)).start();
}
// main 线程代表的是我们的班长
System.out.println(Thread.currentThread().getName() + "班长关门走人");
}
public static void main(String[] args) throws InterruptedException {
// 调用 closeDoor 静态方法
closeDoor();
}
}
这程序够简单吧?我知道你肯定会了,OK 执行一下
??? 班长怎么把三个同学锁在教室了?
这三个同学有点懵,怎么突然关灯锁门了?醉了
有些同学可能会问,会不会是关门的方式不对(哈哈)
OK 我们再执行几次试下:
。。。。。。
这说明我们的程序写得不对,按道理来说应该是全部同学都离开教室才关门的,对吧,所以我们的程序应该是这样写才对
public static void main(String[] args) throws InterruptedException {
// 创建 CountDownLatch 并初始化大小
CountDownLatch countDownLatch = new CountDownLatch(8);
for (int i = 1; i <= 8; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "号同学离开教室");
// 执行减一操作
countDownLatch.countDown();
},String.valueOf(i)).start();
}
// 唤醒
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "班长关门走人");
}
OK 运行一下
这个才是班长的正确关教室门姿势
CountDownLatch 的构造函数
我们 new 一个 CountDownLatch 对象的时候默认是使用下面这个构造函数
// 可以看到这个构造函数必须要传入一个 int 值
public CountDownLatch(int count) {
// 如果传入的 count 值小于 0 的话会抛出 IllegalArgumentException 异常
if (count < 0) throw new IllegalArgumentException("count < 0");
// 否则就创建一个静态内部类 Sync
this.sync = new Sync(count);
}
构造器中的计数值(count)实际上就是闭锁需要等待的线程数量 。这个值只能被设置一次,而且CountDownLatch没有提供任何机制去重新设置这个计数值
静态内部类 Sync
CountDownLatch 的静态内部类 Sync 继承了 AQS 类(AbstractQueuedSynchronizer),会重写 AQS 类中的一些重要方法
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
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;
}
}
}
CountDownLatch 常用的方法
CountDownLatch 主要有两个方法,当一个或多个线程调用 await() 方法时,这些线程会阻塞
- 其它线程调用 countDown() 方法会将计数器减 1(调用 countDown 方法的线程不会阻塞)
- 当计数器的值变为 0 时,因 await() 方法阻塞的线程会被唤醒,继续执行
await() 方法
await() 方法是获取一个共享锁
acquireSharedInterruptibly(int arg) 方法是 AQS 中定义的,该方法是被 final 修饰的,无法被重写,但是子类可以重写里面调用的 tryAcquireShared(arg) 方法
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
acquireSharedInterruptibly(int arg)
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
tryAcquireShared(int arg)
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
tryAcquireShared(int acquires)
当计数器的值为 0 的时候返回 1 : 表示获取锁成功,直接返回,线程可继续操作
当计数器的值不为 0 的时候返回 -1 : 表示获取锁失败,开始进入队列中排队等待,然后会继续按照 AQS 中的 acquireSharedInterruptibly 方法中的逻辑,执行doAcquireSharedInterruptibly(int arg)
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
doAcquireSharedInterruptibly(int arg)
AQS 中 doAcquireSharedInterruptibly(int arg) 实现如下 (该方法为一个自旋方法会尝试一直去获取同步状态)
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);
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);
}
}
parkAndCheckInterrupt()
调用 LockSupport 的 park() 方法,表示禁用当前线程
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
countDown() 方法
CountDownLatch 的 countDown() 方法如下:
方法递减锁存器的技术,如果计数到达零,则释放所有等待的线程(释放共享锁),也就是上述例子中的 main 线程
public void countDown() {
sync.releaseShared(1);
}
releaseShared(int arg)
AQS 中的 releaseShared 方法实现如下:
同样地,releaseShared 方法是被 final 所修饰的,不能被重写,但是 CountDownLatch 的内部类 Sync 重写了 tryReleaseShared
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
tryReleaseShared(int releases)
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
// 获取锁状态
int c = getState();
if (c == 0)
// 如果计数器为 0 ,说明释放锁成功,直接返回
return false;
// 将计数器减一
int nextc = c-1;
// 使用 CAS 更新计算器的值
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
执行 AQS 中 releaseShared方法中的 doReleaseShared方法 去释放锁信息
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
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;
}
}
总结
-
CountDownLatch 内部通过共享锁实现。在创建CountDownLatch实例时,需要传递一个int型的参数:count,该参数为计数器的初始值,也可以理解为该共享锁可以获取的总次数。
-
当某个线程调用 await() 方法时,程序首先判断 count 的值是否为0,如果不为 0 的话则会一直等待直到 count 为 0
-
当其他线程调用 countDown() 方法时,则执行释放共享锁状态,使 count 值 – 1
-
当在创建 CountDownLatch 时初始化的 count 参数,必须要有 count 线程调用 countDown方法才会使计数器 count 等于 0,锁才会释放,前面等待的线程才会继续运行
注意:CountDownLatch 不能回滚重置