20大进阶架构专题每日送达
并发编程中常遇到这种情况,一个线程需要等待另外多个线程执行后再执行。遇到这种情况你一般怎么做呢?今天就介绍一种JDk提供的解决方案来优雅的解决这一问题,那就是倒计时器CountDownLatch。本文将分以下两部分介绍:
CountDownLatch的作用是让线程等待其它线程完成一组操作后才能执行,否则就一直等待。
举个开会的例子:
老板不能一来就开会,必须要等员工都到了再开会,用CountDownLatch实现如下:
public class CountDownLatchTest {
private static CountDownLatch countDownLatch = new CountDownLatch(5);
// Boss线程,等待员工到齐开会
static class BossThread extends Thread {
@Override
public void run() {
System.out.println("Boss进入会议室准备材料...");
System.out.println("Boss在会议室等待...");
try {
countDownLatch.await(); // Boss等待
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Boss等到人齐了,开始开会...");
}
}
// 员工到达会议室
static class EmpleoyeeThread extends Thread {
@Override
public void run() {
System.out.println("员工" + Thread.currentThread().getName()
+ ",到达会议室....");
countDownLatch.countDown();
}
}
public static void main(String[] args) {
// Boss线程启动
new BossThread().start();
// 员工到达会议室
for (int i = 0; i < countDownLatch.getCount(); i++) {
new EmpleoyeeThread().start();
}
}
}
控制台输出:
Boss进入会议室准备材料...
Boss在会议室等待...
员工Thread-2,到达会议室....
员工Thread-3,到达会议室....
员工Thread-4,到达会议室....
员工Thread-1,到达会议室....
员工Thread-5,到达会议室....
Boss等到人齐了,开始开会...
总结CountDownLatch的使用步骤:(比如线程A需要等待线程B和线程C执行后再执行)
await()
挂起;countDown()
,使N-1;线程C执行后调用countDown()
,使N-1;countDown()
后检查N=0了,唤醒线程A,在await()
挂起的位置继续执行。CountDownLatch是通过一个计数器来实现的,当我们在new 一个CountDownLatch对象的时候需要带入该计数器值,该值就表示了线程的数量。每当一个线程完成自己的任务后,计数器的值就会减1。当计数器的值变为0时,就表示所有的线程均已经完成了任务,然后就可以恢复等待的线程继续执行了。
CountDownLatch只有一个属性Sync,Sync是继承了AQS的内部类。
创建CountDownLatch时传入一个count值,count值被赋值给AQS.state
。
CountDownLatch是通过AQS共享锁实现的,AQS这篇文章中详细讲解了AQS独占锁的原理,AQS共享锁和独占锁原理只有很细微的区别,这里大致介绍下:
acquireSharedInterruptibly()
方法获取不到锁时,线程被构造成结点进入AQS阻塞队列。releaseShared()
方法将当前线程持有的锁彻底释放后,会唤醒AQS阻塞队列中等锁的线程,如果AQS阻塞队列中有连续N个等待共享锁的线程,就将这N个线程依次唤醒。public class CountDownLatch {
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
setState(count);
}
}
private final Sync sync;
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
}
await()
是将当前线程阻塞,理解await()
的原理就是要弄清楚await()是如何将线程阻塞的。
await()
调用的就是AQS获取共享锁的方法。当AQS.state=0
时才能获取到锁,由于创建CountDownLatch时设置了state=count
,此时是获取不到锁的,所以调用await()
的线程挂起并构造成结点进入AQS阻塞队列。
创建CountDownLatch时设置
AQS.state=count
,可以理解成锁被重入了count次。await()
方法获取锁时锁被占用了,只能阻塞。
/**
* CountDownLatch.await()调用的就是AQS获取共享锁的方法acquireSharedInterruptibly()
*/
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
/**
* 获取共享锁
* 如果获取锁失败,就将当前线程挂起,并将当前线程构造成结点加入阻塞队列
* 判断是否获取锁成功的方法由CountDownLatch的内部类Sync实现
*/
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0) // 尝试获取锁的方法由CountDownLatch的内部类Sync实现
doAcquireSharedInterruptibly(arg); // 获取锁失败,就将当前线程挂起,并将当前线程构造成结点加入阻塞队列
}
/**
* CountDownLatch.Sync实现AQS获取锁的方法
* 只有AQS.state=0时获取锁成功。
* 创建CountDownLatch时设置了state=count,调用await()时state不为0,返回-1,表示获取锁失败。
*/
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
countDown()
方法是将count-1,如果发现count=0了,就唤醒阻塞的线程。
countDown()
调用AQS释放锁的方法,每次将state减1。当state减到0时是无锁状态了,就依次唤醒AQS队列中阻塞的线程来获取锁,继续执行逻辑代码。
/**
* CountDownLatch.await()调用的就是AQS释放共享锁的方法releaseShared()
*/
public void countDown() {
sync.releaseShared(1);
}
/**
* 释放锁
* 如果锁被全部释放了,依次唤醒AQS队列中等待共享锁的线程
* 锁全部释放指的是同一个线程重入了N次需要N次解锁,最终将state变回0
* 具体释放锁的方法由CountDownLatch的内部类Sync实现
*/
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 释放锁,由CountDownLatch的内部类Sync实现
doReleaseShared(); // 锁全部释放之后,依次唤醒等待共享锁的线程
return true;
}
return false;
}
/**
* CountDownLatch.Sync实现AQS释放锁的方法
* 释放一次,将state减1
* 如果释放之后state=0,表示当前是无锁状态了,返回true
*/
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false;
// state每次减1
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;// state=0时,无锁状态,返回true
}
}
CountDownLatch用于一个线程A需要等待另外多个线程(B、C)执行后再执行的情况。
创建CountDownLatch时设置一个计数器count,表示要等待的线程数量。线程A调用await()
方法后将被阻塞,线程B和线程C调用countDown()
之后计数器count减1。当计数器的值变为0时,就表示所有的线程均已经完成了任务,然后就可以恢复等待的线程A继续执行了。
CountDownLatch是由AQS实现的,创建CountDownLatch时设置计数器count其实就是设置AQS.state=count
,也就是重入次数。await()
方法调用获取锁的方法,由于AQS.state=count
表示锁被占用且重入次数为count,所以获取不到锁线程被阻塞并进入AQS队列。countDown()
方法调用释放锁的方法,每释放一次AQS.state减1,当AQS.state变为0时表示处于无锁状态了,就依次唤醒AQS队列中阻塞的线程来获取锁,继续执行逻辑代码。
【原创】01|开篇获奖感言 【原创】02|并发编程三大核心问题 【原创】03|重排序-可见性和有序性问题根源 【原创】04|Java 内存模型详解 【原创】05|深入理解 volatile 【原创】06|你不知道的 final 【原创】07|synchronized 原理 【原创】08|synchronized 锁优化 【原创】09|基础干货 【原创】10|线程状态 【原创】11|线程调度 【原创】12|揭秘 CAS 【原创】13|LockSupport 【原创】14|AQS 源码分析 【原创】15|重入锁 ReentrantLock 【原创】16|公平锁与非公平锁 【原创】17|读写锁八讲(上) 【原创】18|读写锁八讲(下) 【原创】19|JDK8新增锁StampedLock 【原创】20|StampedLock源码解析