一、闲说题外话
Doug Lea 很多并发的工具类。其中 AQS 框架,最为有名,面试问的也最多, 为啥呢?
ConCurrentHashMap
是 Doug Lea 写的,
面试都问烂了,就算没看过源码,也能编几句,给糊弄过去。
真要是看过源码,里面大量位运算的代码,不好描述,面试官会听晕的。
ThreadPoolExecutor
也是 Doug Lea 写的,
同样是面试必问,原理很复杂,仅仅是说清楚源码就不容易了,
细节点很多,你要是看过源码,找一个冷门的,能把面试官给问死。
这多尴尬,面试官也不会太往深处问。
BlockingQueue
也是 Doug Lea 写的,
它是线程池的底层之一,面试官很喜欢问常见的队列有哪些,
你要是回答出来了,会再问你哪些是阻塞队列,哪些是有界队列,
其实这都是皮毛,看点文章就能背出来。底层原理是 ReentrantLock
,
还有就是 Condition
,这两个工具类都算是 基于 AQS 框架实现的。
是的,不用问,都是 Doug Lea 写的 !!!
这位就是传说中的大神——Doug Lea,一看就是文化人的样子。
和蔼可亲,风度翩翩,尤其是那飘逸的头发,让我等程序员好生羡慕
呀!!
哎呀,跑题啦!还说技术
AQS 框架下,有几个重量级的工具类,ReentrantLock
、Semaphore
、CountDownLatch
等等。
这些面试官就特别喜欢问,原理较简单,几句话就能说清楚,
会就是会,不会就是不会,立马见水准。
AQS框架,平日程序员写业务代码,不常用,但一旦用了,那肯定是有一定水平的程序员。
能说出点源码细节的,说明这人来时有进取心,会看些高并发的东东。
就像中学生,一说起鲁迅的文章,都会头发麻,
那些所谓的深意,鲁迅当时真的想到了吗?
程序员看到高并发的代码,也会头皮发麻,
简单说就是搞不定,说白了,就是对 Doug Lea 提供的工具类,不熟悉。
说多了都是泪呀!!!!
对大神我是特别的膜拜,就算是要把我给煮了,
我也写不出这牛气冲天的,高并发工具类。
然而,然而,然而,大神写的源码,我仔仔细细看了,
而且,而且,而且,看完我还写了源码的详细解读!
让我叉会儿腰,我骄傲!!! 上面的红色字里,加有超链接哈,
点开就是我写的源码分析,陆陆续续写的源码解读哈!
今天咱们就来说说 CountDownLatch
二、言归正传,CountDownLatch 到底是什么?
先看下官方的解释
* A synchronization aid that allows one or more threads to wait until
* a set of operations being performed in other threads completes.
字面意思就是:
CountDownLatch
是一个并发的工具类,
可以让某个线程等待,等待啥呢?
等待其它线程执行结束,这个线程再开始执行。
这个解释很生硬,说一个现实的场景,你可以体会下。
说一个导游,带了10个人参观景点。导游说自由参观哈,一个小时后,上车去下个景点。
快一个小时了,游客陆陆续续回来了,导游点下人数,人不齐,给司机说等会再出发,
又过了一会,最后一个游客回来了,导游说人齐了,司机大哥开车走人。
这里司机开车到下个景点,你可以认为是一个线程要做的事,
10个游客参观景点,完事回到车上,可以认为是其它 10 个线程。
开车那个线程,要等待 游客的10个线程都结束了,即回到车上,司机才会开车。
否则就一直等待。
CountDownLatch 实现这样的功能,让某个线程一直等待,直到其它线程结束。
三、示例用法
CountDownLatch的源码,除去注释,总共就没几行,原理也很简单,我就多扯些别的。
其实在源码的注释中,已经很详细的交待了,怎么用,还给了例子。
之前我写了一篇《Semaphore 信号量探究》,里面的示例代码,
就很好的体现了 CountDownLatch 的用法。
这里我给出一个,我在工作中用到的一个例子。
一个接口里,我要查询 5 张表的数据,返回给页面,假设这5张表之间,没有依赖,
即 查询的先后顺序不影响结果,那正常就是查 5 次数据库。
这样整个接口在耗时就比较长。想要缩短耗时,打算用多线程的方法查。
保证所有子线程查询结束后,再结束接口的请求。
@SneakyThrows
public static void main(String[] args) {
int count = 4;
ExecutorService pool = Executors.newFixedThreadPool(count);
CountDownLatch countDownLatch = new CountDownLatch(count);
pool.execute(() -> {
selectOne();// 业务代码,查询表 1
countDownLatch.countDown();
});
pool.execute(() -> {
selectTwo();// 业务代码,查询表 2
countDownLatch.countDown();
});
pool.execute(() -> {
selectThree();// 业务代码,查询表 3
countDownLatch.countDown();
});
pool.execute(() -> {
selectFour();// 业务代码,查询表 4
countDownLatch.countDown();
});
selectFive(); // 查表5由主线程执行
countDownLatch.await();
}
这只是有 demo,这里能说明问题就好。
五个任务,开了4个子线程,每个线程执行一个,最后一个,由主线程执行。
插一个问题:五个任务,为什么只开了4个线程?
这里使用的线程池,忽略创建线程的时间,execute
方法的执行时间,是 0.01 毫秒 这个级别的。
连接数据库,查询操作,相对是耗时操作,是 毫秒 级别,或是上 百毫秒 级别的。
大概画了张图来说明问题
用多线程来查询,显然比五个线程来执行五个任务来的快。
多线程版本,假设五个任务中,耗时最长的是 15 毫秒,那总任务耗时大约就是 15 毫秒,
而单线程版本,耗时大概等于,五个任务耗时的总和。
countDownLatch
的作用,就是等子线程任务都执行完,才让主线程将结果返回。
四、原理图解
countDownLatch
也是 AQS
框架的一员,AQS
对象中,有个 state
属性。
在示例代码中,初始化时,state
会被设置为 4,(因为 count
的值是4)
CountDownLatch countDownLatch = new CountDownLatch(count);
执行 countDownLatch.countDown()
时,state
会相应减 1。
当主线程执行了到 countDownLatch.await(); 这行时,
假设子线程中,只有任务3没有执行完,
即线程3还没有调用 countDown()
这个方法,
那此时 state 等于 1,那 await 会被阻塞线程。
-
为什么 state 等于 1?
初始化时,state 等于 4,刚刚我说了,只有任务3没有执行完,
那也就是说,其它三个线程执行完了,调用countDown()
这个方法
调用一次countDown()
,state 就减 1 -
为什么state 等于 1时,await 会阻塞线程?
await
方法执行时,会判断state
的值,若等于0,方法结束,
若大于0,阻塞线程(调用park
方法),等待被唤醒。
至此为止,简单说明了下,主线程等待子线程是怎么实现的。
总结下就是:
- 初始化时,
state
会被赋值 - 子线程调用
countDown()
方法时,会将state
减 1 - 主线程调用
await()
方法时,若state
大于 0 ,线程会被阻塞
这就实现了主线程等待子线程。
当任务 3 执行完,调用了 countDown()
方法时,state
被 减小 1,此时 state 等于 0,
这种情况下,countDown()
内部还会调用 unpark
方法唤醒主线程
关于 park
和 unpark
,不熟悉的,可以看《阻塞与唤醒》
至此,解释了阻塞的线程,是什么时候被唤醒的。
那 countDownLatch
的使用,及其原理,大概就说完了。
开篇插了个问题,5 个任务,为什么只开了4个线程?
了解 await
方法后,你能想出原因么,欢迎在评论区,给出你的想法。
五、源码略讲
countDown
方法,就是将state
减 1.
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
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;
}
}
tryReleaseShared
这个方法比较简单,就是 将 state
减 1,
doReleaseShared
这方法等会再说。
- 下面看
await
方法
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
从源码中看出,当 state 等于 0 时,tryAcquireShared
返回 1,
那 tryAcquireShared(arg) < 0
这个条件就不成立,await 方法就结束了。
当 state
不等于0 时,会执行 doAcquireSharedInterruptibly(arg);
这个方法,在《Semaphore 信号量》 这篇中,具体讲过,这里不讲了。
你想从源码层面理解,翻看这篇文章就好了,反正本文是不细讲了。
只用知道,这个方法会调用 park
方法,使线程阻塞。
简单总结下:await 方法是,判断 state 是否等于0,不等于0 阻塞,等于 0 结束。
- 下面再说下,阻塞之后是怎么唤醒的
前面讲 countDown
时说过,当 state - 1
等于 0 时,
会执行 doReleaseShared
,这个就是唤醒线程的。
private void doReleaseShared() {
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;
}
}
这里 unparkSuccessor
就是唤醒线程。
这个源码,也不细讲了,同样在 《Semaphore 信号量》 详细讲过。
至此为止,CountDownLatch
大概的说了一遍。
讲的很粗略,不是我糊弄人,因为以前的文章,相关的源码已经分析过了。
如果你对 ReentrantLock
、Semaphore
很了解,
可是说已经看了我写的文章,
那CountDownLatch
真的不用再看源码,知道怎么用就可以了。
不信你试试哈!!!