大家好,我是小黑,一个在互联网苟且偷生的农民工。

先问大家一个问题,在主线程中创建多个线程,在这多个线程被启动之后,主线程需要等子线程执行完之后才能接着执行自己的代码,应该怎么实现呢?

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方法;循环判断当前线程是否还活着。什么意思呢?

  1. main线程在调用线程T的join()方法时,会先获取T对象的锁;
  2. 在join方法中会调用T对象的wait()方法等待,而wait()方法会释放T对象的锁,并且main线程在执行完wait()之后会进入阻塞状态;
  3. 最后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的更多相关文章

  1. Java并发编程之CountDownLatch,CyclicBarrier实现一组线程相互等待、唤醒

    java多线程应用场景不少,有时自己编写代码又不太容易实现,好在concurrent包提供了不少实现类,还有google的guava包更是提供了一些最佳实践,这让我们在面对一些多线程的场景时,有了不少 ...

  2. Java并发编程之CountDownLatch

    一.场景描述 在多线程程序设计中,经常会遇到一个线程等待一个或多个线程的场景 例如:百米赛跑,十名运动员同时起跑,由于速度的快慢,肯定有先到达和后到达的,而终点有个统计成绩的仪器,当所有选手到达终点时 ...

  3. 多线程进阶——JUC并发编程之CountDownLatch源码一探究竟

    1.学习切入点 JDK的并发包中提供了几个非常有用的并发工具类. CountDownLatch. CyclicBarrier和 Semaphore工具类提供了一种并发流程控制的手段.本文将介绍Coun ...

  4. 并发编程之CountDownLatch

    在前面的两篇文章中我们分别用volatile.notify()和wait()分别实现了一个场景,我们再来回顾一下前面的场景:在main方法中开启两个线程,其中一个线程t1往list里循环添加元素,另一 ...

  5. Java并发编程之CountDownLatch的用法

    一.含义 CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能.CountDownLatch是一个同步的辅助类,它可以允许一个或多个线程等待, ...

  6. 并发编程之 Exchanger 源码分析

    前言 JUC 包中除了 CountDownLatch, CyclicBarrier, Semaphore, 还有一个重要的工具,只不过相对而言使用的不多,什么呢? Exchange -- 交换器.用于 ...

  7. 并发编程之J.U.C的第二篇

    并发编程之J.U.C的第二篇 3.2 StampedLock 4. Semaphore Semaphore原理 5. CountdownLatch 6. CyclicBarrier 7.线程安全集合类 ...

  8. [转载]并发编程之Operation Queue和GCD

    并发编程之Operation Queue http://www.cocoachina.com/applenews/devnews/2013/1210/7506.html 随着移动设备的更新换代,移动设 ...

  9. Java并发编程之CAS

    CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术.简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替 ...

随机推荐

  1. 当vue 页面加载数据时显示 加载loading

    参考:https://www.jianshu.com/p/104bbb01b222 Vue 页面加载数据之前增加 `loading` 动画 创建组件 1.新建 .vue 文件: src -> c ...

  2. Apache Flink上传路径遍历(CVE-2020-17518)

    影响版本 Flink1.5.1-1.11.2 复现 POST /jars/upload HTTP/1.1 Host: localhost:8081 Accept-Encoding: gzip, def ...

  3. 2021年最新字节跳动Android面试真题解析

    概述 时间过得是真TM快,回想自己是16年从学校毕业,现在是出来工作的第五个年头啦.在不同的大小公司都待过,就在前段时间顺利的完成了一次跳槽涨薪,面试了几家公司,最终选择了字节跳动.今特此前来跟大家进 ...

  4. Git8.3k星,十万字Android主流开源框架源码解析,必须盘

    为什么读源码 很多人一定和我一样的感受:源码在工作中有用吗?用处大吗?很长一段时间内我也有这样的疑问,认为哪些有事没事扯源码的人就是在装,只是为了提高他们的逼格而已. 那为什么我还要读源码呢?一刚开始 ...

  5. Linux守护进程及Systemd

    当我们启动一个前台任务后,命令行窗口退出,应用也就一起退出,无法访问了.怎么才能让它变成系统的守护进程(daemon),成为一种服务(service),一直在那里运行呢? 守护进程 前台任务和后台任务 ...

  6. dubbo学习实践(5)之Dubbo-Admin元数据中心配置(zookeeper&Redis&Consul)

    1.Dubbo2.7.8元数据中心配置zookeeper版 前面文章已经写到了dubbo-admin管理平台的docker版配置及dubbo服务注册与调用,这篇文章记录dubbo元数据中心配置 翻开d ...

  7. SpringCloud升级之路2020.0.x版-13.UnderTow 核心配置

    本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford Undertow ...

  8. Linux平台上转换文件编码

    Linux系统的iconv指令是一个很好的文件编码转换工具,支持的编码范围广,使用方便,例如将一个utf-8编码的文件(名为tic)转换为gbk编码: iconv -f utf-8 -t gbk ti ...

  9. 渲染优化之CSS Containment

    引言 在开始介绍今天的主角 CSS Containment 之前,我们需要了解一些前置知识回流和重绘,方便我们理解以及应用的场景. 简单回忆下回流和重绘 回流(Reflow):当浏览器必须重新处理和绘 ...

  10. 安全工具推荐之sqlmap tamper&sqlmap api

    我发现总有一些人喜欢问sqlmap的tamper脚本,问完工具问参数,问完参数问脚本...... 你这个问题问的水平就很艺术,让我一时不知从何说起...... 说一下在sqlmap的使用过程中,个人了 ...