并发编程之:CountDownLatch
大家好,我是小黑,一个在互联网苟且偷生的农民工。
先问大家一个问题,在主线程中创建多个线程,在这多个线程被启动之后,主线程需要等子线程执行完之后才能接着执行自己的代码,应该怎么实现呢?
Thread.join()
看过我 并发编程之:线程 的朋友应该知道怎么做,在Thread类中有一个方法join(),这个方法是一个阻塞方法,当前线程会等待调动join()方法的线程死亡之后再继续执行。

我们通过代码来看看执行结果。
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
Thread t = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " run ~");
});
t.start();
t.join();
}
System.out.println("main线程执行结束");
}
}

从结果可以看出,main线程要等到所有子线程都执行完之后才会继续执行,并且每一个子线程是按顺序执行的。
我们在来看一下join()方法是如何让主线程阻塞的呢?来看一下源码。
public final void join() throws InterruptedException {
// 默认传入0毫秒
join(0);
}
// 本方法是synchronized的
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
// 测试当前线程是否还活着
while (isAlive()) {
// 执行wait,当前线程等待
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
从join方法的源码中我们可以看到几个重要的信息,首先join()方法默认是等待0毫秒;join(long millis)方法是一个synchronized方法;循环判断当前线程是否还活着。什么意思呢?
- main线程在调用线程T的join()方法时,会先获取T对象的锁;
- 在join方法中会调用T对象的wait()方法等待,而wait()方法会释放T对象的锁,并且main线程在执行完wait()之后会进入阻塞状态;
- 最后main线程在被notify唤醒之后,需要再循环判断T对象是否还活着,如果还活着会再次执行wait()。
而在线程执行完run()方法之后,JVM会调用该线程的exit()方法,通过notifyAll()唤醒处于等待状态的线程。
private void exit() {
if (group != null) {
// 终止group中的线程this
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
void threadTerminated(Thread t) {
synchronized (this) {
remove(t);
if (nthreads == 0) {
// 唤醒等待线程
notifyAll();
}
if (daemon && (nthreads == 0) &&
(nUnstartedThreads == 0) && (ngroups == 0))
{
destroy();
}
}
}
细心的话你会发现,使用Thread.join()只能做到让一个线程执行完之后,做不到同时等待多个线程,比如我们上面的代码,线程1执行完之后才能执行线程2,无法做到让线程1和线程2同时处理。
CountDownLatch
而在JUC包中的工具类CountDownLatch具备和Thread.join()方法同样的能力,可以等待一个线程执行完之后再处理,并且支持同时等待多个线程。我们来修改一下上面Thread.join()的例子。
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
Thread t = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " run ~");
countDownLatch.countDown();
});
t.start();
}
countDownLatch.await();
System.out.println("main线程执行结束");
}
}

CountDownLatch需要在创建时指定一个计数值,在子线程中执行完之后调用countDown()方法进行递减,主线程的await()方法会等到值减为0之后继续执行。
从运行结果我们可以看到,100个子线程并不是按顺序执行的,而是随机的。
我们通过CountDownLatch的源码来看一下是如何实现的。
private final Sync sync;
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
在CountDownLatch中我们看到有一个Sync变量,从上一期AQS源码解析内容中我们知道Sync是AQS的一个子类实现;
首先构造方法传入的count值会作为参数赋值给Sync中的state变量。
然后我们来看一下在线程中的CountDownLath.countDown()方法会做些什么事情。
public void countDown() {
// 释放共享锁
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
如果有看我上期AQS源码解析的同学一定很熟悉,这段代码就是共享锁的解锁过程,本质上就是对state-1。
那么主线程是如何实现的等待呢?我们猜一下,应该是去判断state有没有减为0,如果减为0则代表所有的线程都执行完countDown()方法,则可以继续执行,如果state还不等于0,则表示还有线程正在执行,等待就OK啦。
我们来看看源码,是否和我们猜想的一样呢?
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)
// state还不是1
doAcquireSharedInterruptibly(arg);
}
// 获取锁状态,当state减为0时,返回1
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
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;
}
}
// 线程在这里park
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
可以发现await()方法和我们昨天看到的共享锁解锁过程一模一样,符合我们的猜想。
所以,CountDownLatch的底层实现也是依靠AQS来完成的,现在大家肯定对于AQS有更深刻的认识了。
区别
我们现在来对比一下Thread.join()和CountDownLatch有哪些区别:
- Thread.join()是Thread类的一个方法,而CountDownLatch是JUC包中的一个工具类;
- Thread.join()的实现是依靠Object的wait()和notifyAll()来完成的,而CountDownLatch是通过AQS完成的;
- Thread.join()只支持让一个线程等待,不支持同时等待多个线程,而CountDownLatch可以支持,所以CountDownLatch的效率要更高。
好的,本期内容就到这里,我们下期见。
并发编程之:CountDownLatch的更多相关文章
- Java并发编程之CountDownLatch,CyclicBarrier实现一组线程相互等待、唤醒
java多线程应用场景不少,有时自己编写代码又不太容易实现,好在concurrent包提供了不少实现类,还有google的guava包更是提供了一些最佳实践,这让我们在面对一些多线程的场景时,有了不少 ...
- Java并发编程之CountDownLatch
一.场景描述 在多线程程序设计中,经常会遇到一个线程等待一个或多个线程的场景 例如:百米赛跑,十名运动员同时起跑,由于速度的快慢,肯定有先到达和后到达的,而终点有个统计成绩的仪器,当所有选手到达终点时 ...
- 多线程进阶——JUC并发编程之CountDownLatch源码一探究竟
1.学习切入点 JDK的并发包中提供了几个非常有用的并发工具类. CountDownLatch. CyclicBarrier和 Semaphore工具类提供了一种并发流程控制的手段.本文将介绍Coun ...
- 并发编程之CountDownLatch
在前面的两篇文章中我们分别用volatile.notify()和wait()分别实现了一个场景,我们再来回顾一下前面的场景:在main方法中开启两个线程,其中一个线程t1往list里循环添加元素,另一 ...
- Java并发编程之CountDownLatch的用法
一.含义 CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能.CountDownLatch是一个同步的辅助类,它可以允许一个或多个线程等待, ...
- 并发编程之 Exchanger 源码分析
前言 JUC 包中除了 CountDownLatch, CyclicBarrier, Semaphore, 还有一个重要的工具,只不过相对而言使用的不多,什么呢? Exchange -- 交换器.用于 ...
- 并发编程之J.U.C的第二篇
并发编程之J.U.C的第二篇 3.2 StampedLock 4. Semaphore Semaphore原理 5. CountdownLatch 6. CyclicBarrier 7.线程安全集合类 ...
- [转载]并发编程之Operation Queue和GCD
并发编程之Operation Queue http://www.cocoachina.com/applenews/devnews/2013/1210/7506.html 随着移动设备的更新换代,移动设 ...
- Java并发编程之CAS
CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术.简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替 ...
随机推荐
- Skywalking-02:如何写一个Skywalking trace插件
如何写一个Skywalking trace插件 javaagent 原理 美团技术团队-Java 动态调试技术原理及实践 类图 实现 ConsumeMessageConcurrentlyInstrum ...
- jvm源码解读--06 Method 方法解析
进入 // Methods bool has_final_method = false; AccessFlags promoted_flags; promoted_flags.set_flags(0) ...
- .NET5控制台程序使用EF连接MYSQL
.NET5控制台程序使用EF连接MYSQL 1.使用nuget下载: Microsoft.Extensions.Configuration.Json Pomelo.EntityFrameworkCor ...
- 防火墙和SElinux简单配置
1.查看防火墙状态 # firewall-cmd --state & #systemctl status firewalld.service 2.停止与开启firew ...
- configmap使用方法
说明: kubernetes统一配置管理方案configmap,实现将配置文件从容器镜像中解耦,增强应用的可移植性.数据可直接注入pod对象中,为容器所使用,注入方式有挂载为存储卷和传递为环境变量两种 ...
- 21JavaScript笔记(1)
JavaScript 基于对象和事件驱动 简单描述性语言 函数优先 解释型(即时编译型) 具有安全性的脚本语言 1.js组成 核心语法(ECMAScript):开放的.标准的脚本语言规范,主要包含了语 ...
- 安鸾CTF Writeup wordpress 01
题目一: wordpress 01 URL:http://whalwl.site:8041/ wordpress 站思路就是先用wpscan 进行扫描检测一遍. wpscan 使用方法可以参考两篇文章 ...
- Layui-自定义函数及调用
控件 表格 时间范围 页面展示 场景 页面中选择开始时间和结束时间表格变化 使用 html代码 <div> <form class="layui-form" ac ...
- netty系列之:在netty中使用protobuf协议
目录 简介 定义protobuf 定义handler 设置ChannelPipeline 构建client和server端并运行 总结 简介 netty中有很多适配不同协议的编码工具,对于流行的goo ...
- NOIP 模拟 $24\; \rm block$
题解 \(by\;zj\varphi\) 因为它要求大于它的且放在它前的数的个数要小于它的 \(key\) 值,所以先按 \(\rm val\) 值排序,然后按 \(\rm key\) 值排序,按顺序 ...