基础用法
- 初始化一个 计数器(count)。
- 线程调用
await()
会阻塞,直到计数器变成 0。同时还可以支持带超时时间的await()。 - 其他线程可以调用
countDown()
让计数器减 1。 - 当计数器变成 0 时,所有因
await()
阻塞的线程会被同时唤醒。
流程如下:
scss
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(3);
new Thread(() -> {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "执行");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
countDownLatch.countDown();
}
}, "t1").start();
new Thread(() -> {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "执行");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
countDownLatch.countDown();
}
}, "t2").start();
new Thread(() -> {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "执行");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
countDownLatch.countDown();
}
}, "t3").start();
Thread.currentThread().setName("main");
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "执行");
}
详细说明:
- 首先new了一个CountDownLatch,计数器初始化为3
- 接下来新建三个线程执行任务,每个任务各自结束时执行countDown。此时主主线程调用await(),处于阻塞状态
- 三个线程任务执行完毕,count == 0,主线程停止阻塞执行任务
源码详解
属性解释
想要了解CountDownLatch,首先要了解AQS是什么。接下来对其进行简要说明。
AQS(AbstractQeueudSynchronizer)抽象队列同步器,是CLH锁的改进。是由Node组成的双向队列。
可以看到,Node内部属性主要由等待线程、前驱节点,后继节点,以及status构成。前驱节点与后继节点无需过多说明,主要来关注status代表什么,有什么作用。
status有三种状态:
WAITTING:为1。节点已经被唤醒,正在主队列中等待获取锁
COND:为2。表示节点正在条件队列中等待,等待某个条件成立
CANCELLED:为负数。表示取消,遍历时快速跳过
可以看到,CountDownLatch的同步控制就是通过Sync类实现的,Sync又继承了AQS。
初始化
arduino
private final Sync sync;
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
scss
Sync(int count) {
setState(count);
}
初始化CountDownLatch时需要传入计数,然后借此计数new一个Sync出来。Sync的构造函数表明就是将AQS队列的状态设为count。
await()
csharp
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
先调用了AQS的acquireSharedInterruptibly方法,这是AQS提供的共享模式并且可中断的获取资源的方法。可以冲进去看一眼。
java
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;//判断计数器代表的state是否为0,即要等待执行完的线程已经全部执行完
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted() ||
(tryAcquireShared(arg) < 0 &&
acquire(null, arg, true, true, false, 0L) < 0))
throw new InterruptedException();
}
可以看到,他会进行条件判断,如果线程的状态是可以打断的,则直接抛异常。
tryAcquireShared则表示判断查询锁的状态是否允许在共享模式下获取它(计数器为0时 ),如果不能获取,则调用acquire来获取锁资源或者入队。如果acquire方法也无法获取资源或者入队,则抛异常
acquire方法简单解释:
- 判断当前节点是否为头节点
- 如果是头节点或者还没有入队,则直接尝试获取锁资源
- 获取锁资源失败,将直接入队,阻塞线程直到被唤醒或中断
经过tryAcquireShared和acquire两轮成功判断,调用latch.await()的线程要么直接获取到了锁资源(tryAcquireShared -> state = 0),要么进入了阻塞队列(tryAcquireShared -> state > 0 && acquire != 0 )。
countDown()
调用的是AQS的releaseShared(int arg)方法,如果成功释放锁资源,则唤醒下一个节点。
arduino
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false;//计数器中代表的线程 已经全部执行,再释放就返回false
int nextc = c - 1;//计数器 -1
if (compareAndSetState(c, nextc))
return nextc == 0;//通过CAS,判断当前线程是否为最后一个释放的线程
}
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {//如果是最后一个释放的线程,则直接唤醒调用await()的资源
signalNext(head);
return true;
}
return false;
}
从源码可以看到,每有一个线程调用countDown,就会通过CAS将计数器-1直至0。
当减到0的时候,会进行判断并唤醒阻塞队列中的线程。
因为只有一个线程调用了await()方法,所以唤醒的线程必定是此线程。然后调用latch.await()的线程恢复正常执行。
总结流程
我将用上文所举例的代码总结流程。
这是执行结果。
首先初始化计数器为3,即status = 3。新建三个线程分别执行各自的方法,执行完毕时调用countDown()。
主线程进入await()方法,如果打断标记为true,则直接抛异常。然后调用tryAcquireShared()发现status如果为零,则直接执行主线程的方法。如果不为0,因此调用acquire尝试入队。如果入队失败,直接抛异常。否则当前线程入队成功。
新建的三个线程分别异步执行。当t2执行结束后判断status == 0,然后就调用signal方法将阻塞队列中唯一的main线程唤醒,从而调用main方法。
这就是CountDownLatch的全部执行流程。