面试题:如何能够保证T2在T1执行完后执行,T3在T2执行完后执行?——CountDownLatch原理

CountDownLatch的使用方式

CountDownLatch用于某个线程等待其他线程执行完任务再执行,与thread.join()功能类似。常见的应用场景是开启多个线程同时执行某个任务,等到所有任务执行完再执行特定操作,如汇总统计结果。

面试题:如何能够保证T2在T1执行完后执行,T3在T2执行完后执行?

join方法

可以使用join方法解决这个问题。比如在线程A中,调用线程B的join方法表示的意思就是: A等待B线程执行完毕后(释放CPU执行权),在继续执行。

public class RunnableJob {public static void main(String[] args) throws InterruptedException {Worker runnableJob = new Worker();Thread t1 = new Thread(runnableJob, "T1");Thread t2 = new Thread(runnableJob, "T2");Thread t3 = new Thread(runnableJob, "T3");t1.start();//这里就是在main主线程中,调用t1线程的join方法。//也就是main主线程要等待t1执行完成后才能继续往下执行t1.join();t2.start();t2.join();t3.start();t3.join();System.out.println("主线程执行完毕----");}
}
class Worker implements Runnable{public void run() {Thread thread = Thread.currentThread();try {Thread.sleep(1000);System.out.println(thread.getName()+"正在执行");} catch (InterruptedException e) {e.printStackTrace();}}
}//输出
T1正在执行
T2正在执行
T3正在执行
主线程执行完毕----

CountDownLatch

倒计时计数器

CountDownLatch用于某个线程等待其他线程执行完任务再执行,可以被认为是加强版的join()。

public class CountDownLatchTest {public static void main(String[] args) {final CountDownLatch countDownLatch = new CountDownLatch(3);new Thread("T1"){public void run() {try {Thread.sleep(3000);System.out.println(Thread.currentThread().getName()+"正在执行");countDownLatch.countDown();} catch (InterruptedException e) {e.printStackTrace();}};}.start();new Thread("T2"){public void run() {try {Thread.sleep(3000);System.out.println(Thread.currentThread().getName()+"正在执行");countDownLatch.countDown();} catch (InterruptedException e) {e.printStackTrace();}};}.start();new Thread("T3"){public void run() {try {Thread.sleep(3000);System.out.println(Thread.currentThread().getName()+"正在执行");countDownLatch.countDown();} catch (InterruptedException e) {e.printStackTrace();}};}.start();System.out.println("等待三个线程执行完,主线程才能执行");try {//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行;//或者等待timeout时间后count值还没变为0的话也会继续执行countDownLatch.await();
//            countDownLatch.await(20000, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("主线程执行完毕");}
}//输出
等待三个线程执行完,主线程才能执行
T1正在执行
T3正在执行
T2正在执行
主线程执行完毕

调用了await后,主线程被挂起,它会等待直到count值为0才继续执行;因此只影响主线程的执行顺序一定要在T1 T2 T3之后,但T1 T2 T3之间的顺序互不影响

应用场景: 开启多个线程同时执行某个任务,等到所有任务执行完再执行特定操作,如汇总统计结果。

两者区别

相同点:都能等待一个或者多个线程执行完成操作,比如等待三个线程执行完毕后,第四个线程才能执行

不同点:join能让线程按我们预想的的顺序执行,比如线程1执行完了,线程2才能执行,线程2执行完,线程3才能执行,但是CountDownLatch就做不到.

当调用CountDownLatch的countDown方法时,N就会减1,CountDownLatch的await方法会阻塞当前线程,直到N变为零(也就是线程都执行完了),由于countDown方法可以用在任何地方,所以这里说的N个点,可以是N个线程,也可以是1个线程里的N个执行步骤。用在多线程时,只需把这个CountDownLatch的引用传递到线程中即可

CountDownLatch原理

从源码可知,其底层是由AQS提供支持,所以其数据结构可以参考AQS的数据结构,而AQS的数据结构核心就是两个虚拟队列: 同步队列sync queue 和条件队列condition queue,不同的条件会有不同的条件队列。

CountDownLatch对AQS的共享方式实现为:CountDownLatch 将任务分为N个子线程去执行,将 state 初始化为 N, N与线程的个数一致,N个子线程是井行执行的,每个子线程都在执行完成后 countDown() 1次, state 执行 CAS 操作并减1。在所有子线程都执行完成(state=0)时会unpark()主线程,然后主线程会从 await()返回,继续执行后续的动作。

CountDownLatch源码分析

类的继承关系

CountDownLatch没有显示继承哪个父类或者实现哪个父接口, 它底层是AQS是通过内部类Sync来实现的。

public class CountDownLatch {}

类的内部类

CountDownLatch类存在一个内部类Sync,继承自AbstractQueuedSynchronizer,其源代码如下。

private static final class Sync extends AbstractQueuedSynchronizer {// 版本号private static final long serialVersionUID = 4982264981922014374L;// 构造器Sync(int count) {setState(count);}// 返回当前计数int getCount() {return getState();}// 试图在共享模式下获取对象状态protected int tryAcquireShared(int acquires) {return (getState() == 0) ? 1 : -1;}// 试图设置状态来反映共享模式下的一个释放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;}}
}

说明: 对CountDownLatch方法的调用会转发到对Sync或AQS的方法的调用,所以,AQS对CountDownLatch提供支持。

类的属性

可以看到CountDownLatch类的内部只有一个Sync类型的属性:

public class CountDownLatch {// 同步队列private final Sync sync;
}

类的构造函数

public CountDownLatch(int count) {if (count < 0) throw new IllegalArgumentException("count < 0");// 初始化状态数this.sync = new Sync(count);
}

说明: 该构造函数可以构造一个用给定计数初始化的CountDownLatch,并且构造函数内完成了sync的初始化,并设置了状态数。

核心函数 - await函数

此函数将会使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。其源码如下

public void await() throws InterruptedException {// 转发到sync对象上sync.acquireSharedInterruptibly(1);
}

说明: 由源码可知,对CountDownLatch对象的await的调用会转发为对Sync的acquireSharedInterruptibly(从AQS继承的方法)方法的调用。

  • acquireSharedInterruptibly源码如下:
public final void acquireSharedInterruptibly(int arg)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();if (tryAcquireShared(arg) < 0)doAcquireSharedInterruptibly(arg);
}

说明: 从源码中可知,acquireSharedInterruptibly又调用了CountDownLatch的内部类Sync的tryAcquireShared和AQS的doAcquireSharedInterruptibly函数。

  • tryAcquireShared函数的源码如下:
protected int tryAcquireShared(int acquires) {return (getState() == 0) ? 1 : -1;
}

说明: 该函数只是简单的判断AQS的state是否为0,为0则返回1,不为0则返回-1。

  • doAcquireSharedInterruptibly函数的源码如下:
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {// 添加节点至等待队列final Node node = addWaiter(Node.SHARED);boolean failed = true;try {for (;;) { // 无限循环// 获取node的前驱节点final Node p = node.predecessor();if (p == head) { // 前驱节点为头节点// 试图在共享模式下获取对象状态int r = tryAcquireShared(arg);if (r >= 0) { // 获取成功// 设置头节点并进行繁殖setHeadAndPropagate(node, r);// 设置节点next域p.next = null; // help GCfailed = false;return;}}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()) // 在获取失败后是否需要禁止线程并且进行中断检查// 抛出异常throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}
}

说明: 在AQS的doAcquireSharedInterruptibly中可能会再次调用CountDownLatch的内部类Sync的tryAcquireShared方法和AQS的setHeadAndPropagate方法。

  • setHeadAndPropagate方法源码如下。
private void setHeadAndPropagate(Node node, int propagate) {// 获取头节点Node h = head; // Record old head for check below// 设置头节点setHead(node);/** Try to signal next queued node if:*   Propagation was indicated by caller,*     or was recorded (as h.waitStatus either before*     or after setHead) by a previous operation*     (note: this uses sign-check of waitStatus because*      PROPAGATE status may transition to SIGNAL.)* and*   The next node is waiting in shared mode,*     or we don't know, because it appears null** The conservatism in both of these checks may cause* unnecessary wake-ups, but only when there are multiple* racing acquires/releases, so most need signals now or soon* anyway.*/// 进行判断if (propagate > 0 || h == null || h.waitStatus < 0 ||(h = head) == null || h.waitStatus < 0) {// 获取节点的后继Node s = node.next;if (s == null || s.isShared()) // 后继为空或者为共享模式// 以共享模式进行释放doReleaseShared();}
}    

说明: 该方法设置头节点并且释放头节点后面的满足条件的结点,该方法中可能会调用到AQS的doReleaseShared方法,其源码如下。

private void doReleaseShared() {/** Ensure that a release propagates, even if there are other* in-progress acquires/releases.  This proceeds in the usual* way of trying to unparkSuccessor of head if it needs* signal. But if it does not, status is set to PROPAGATE to* ensure that upon release, propagation continues.* Additionally, we must loop in case a new node is added* while we are doing this. Also, unlike other uses of* unparkSuccessor, we need to know if CAS to reset status* fails, if so rechecking.*/// 无限循环for (;;) {// 保存头节点Node h = head;if (h != null && h != tail) { // 头节点不为空并且头节点不为尾结点// 获取头节点的等待状态int ws = h.waitStatus; if (ws == Node.SIGNAL) { // 状态为SIGNALif (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 不成功就继续continue;            // loop to recheck cases// 释放后继结点unparkSuccessor(h);}else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) // 状态为0并且不成功,继续continue;                // loop on failed CAS}if (h == head) // 若头节点改变,继续循环  break;}
}

说明: 该方法在共享模式下释放。

所以,对CountDownLatch的await调用大致会有如下的调用链。

说明: 上图给出了可能会调用到的主要方法,并非一定会调用到

核心函数 - countDown函数

此函数将递减锁存器的计数,如果计数到达零,则释放所有等待的线程

public void countDown() {sync.releaseShared(1);
}

说明: 对countDown的调用转换为对Sync对象的releaseShared(从AQS继承而来)方法的调用。

  • releaseShared源码如下
public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) { // 当state状态为0了,才会执行这里doReleaseShared();return true;}return false;
}

说明: 此函数会以共享模式释放对象,并且在函数中会调用到CountDownLatch的tryReleaseShared函数,并且可能会调用AQS的doReleaseShared函数。

  • tryReleaseShared源码如下
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;}
}

说明: 此函数会试图设置状态来反映共享模式下的一个释放。具体的流程在下面的示例中会进行分析。

  • AQS的doReleaseShared的源码如下
private void doReleaseShared() {/** Ensure that a release propagates, even if there are other* in-progress acquires/releases.  This proceeds in the usual* way of trying to unparkSuccessor of head if it needs* signal. But if it does not, status is set to PROPAGATE to* ensure that upon release, propagation continues.* Additionally, we must loop in case a new node is added* while we are doing this. Also, unlike other uses of* unparkSuccessor, we need to know if CAS to reset status* fails, if so rechecking.*/// 无限循环for (;;) {// 保存头节点Node h = head;if (h != null && h != tail) { // 头节点不为空并且头节点不为尾结点// 获取头节点的等待状态int ws = h.waitStatus; if (ws == Node.SIGNAL) { // 状态为SIGNALif (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 不成功就继续continue;            // loop to recheck cases// 释放后继结点unparkSuccessor(h);}else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) // 状态为0并且不成功,继续continue;                // loop on failed CAS}if (h == head) // 若头节点改变,继续循环  break;}
}

说明: 此函数在共享模式下释放资源。

所以,对CountDownLatch的countDown调用大致会有如下的调用链

示例

下面给出了一个使用CountDownLatch的示例。

import java.util.concurrent.CountDownLatch;class MyThread extends Thread {private CountDownLatch countDownLatch;public MyThread(String name, CountDownLatch countDownLatch) {super(name);this.countDownLatch = countDownLatch;}public void run() {System.out.println(Thread.currentThread().getName() + " doing something");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " finish");countDownLatch.countDown();}
}public class CountDownLatchDemo {public static void main(String[] args) {CountDownLatch countDownLatch = new CountDownLatch(2);MyThread t1 = new MyThread("t1", countDownLatch);MyThread t2 = new MyThread("t2", countDownLatch);t1.start();t2.start();System.out.println("Waiting for t1 thread and t2 thread to finish");try {countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}            System.out.println(Thread.currentThread().getName() + " continue");        }
}

运行结果(某一次):

Waiting for t1 thread and t2 thread to finish
t1 doing something
t2 doing something
t1 finish
t2 finish
main continue

说明: 本程序首先计数器初始化为2。根据结果,可能会存在如下的一种时序图。

说明: 首先main线程会调用await操作,此时main线程会被阻塞,等待被唤醒,之后t1线程执行了countDown操作,最后,t2线程执行了countDown操作,此时main线程就被唤醒了,可以继续运行。下面,进行详细分析。

  • main线程执行countDownLatch.await操作,主要调用的函数如下。

说明: 在最后,main线程就被park了,即禁止运行了。此时Sync queue(同步队列)中有两个节点,AQS的state为2,包含main线程的结点的nextWaiter指向SHARED结点。

  • t1线程执行countDownLatch.countDown操作,主要调用的函数如下。

说明: 此时,Sync queue队列里的结点个数未发生变化,但是此时,AQS的state已经变为1了。

  • t2线程执行countDownLatch.countDown操作,主要调用的函数如下。

说明: 经过调用后,AQS的state为0,并且此时,main线程会被unpark,可以继续运行。当main线程获取cpu资源后,继续运行。

  • main线程获取cpu资源,继续运行,由于main线程是在parkAndCheckInterrupt函数中被禁止的,所以此时,继续在parkAndCheckInterrupt函数运行。

说明: main线程恢复,继续在parkAndCheckInterrupt函数中运行,之后又会回到最终达到的状态为AQS的state为0,并且head与tail指向同一个结点,该节点的额nextWaiter域还是指向SHARED结点

面试题专栏

Java面试题专栏已上线,欢迎访问。

  • 如果你不知道简历怎么写,简历项目不知道怎么包装;
  • 如果简历中有些内容你不知道该不该写上去;
  • 如果有些综合性问题你不知道怎么答;

那么可以私信我,我会尽我所能帮助你。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/57449.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

最新Java零基础知识(持续更新中......)

1. 学习前的准备 一个好的学习方法&#xff08;如何更高效学习&#xff09;&#xff1a; 成为一名合格的程序员&#xff0c;需要具备两个关键能力&#xff1a; 指法速度&#xff1a;高效的代码输入速度。编程思想&#xff1a;能够用编程的方式解决现实问题的能力。 指法速度&am…

SpringBoot中的Component和ComponentScan注解工作原理

Spring IoC 容器的工作是通过管理对象的生命周期和配置来保持业务逻辑清晰&#xff0c;但是 Spring 容器并不会自动知道要管理哪些 bean。所以我们来告诉 Spring 应该处理哪些 bean 以及如何处理&#xff0c;很简单这就是 Spring 的 Component 和 ComponentScan 注释的作用所在…

算法题总结(二十)——并查集

并查集理论基础 并查集常用来解决集合连通性问题&#xff0c;两个节点在不在一个集合&#xff0c;也可以将两个节点添加到一个集合中。 大白话就是当我们需要判断两个元素是否在同一个集合里的时候&#xff0c;我们就要想到用并查集。 并查集主要有两个功能&#xff1a; 将…

linux介绍与基本指令

前言 本次博客将会讲解linux的来源历史、linux操作系统的理解以及它的一些基本指令。 1.linux的介绍 linux的来源 linux的来源最初还是要说到unix操作系统的。 1968年&#xff0c;一些来自通用电器公司、贝尔实验室和麻省理工学院的研究人员开发了一个名叫Multics的特殊操作…

C语言 | Leetcode C语言题解之第502题IPO

题目&#xff1a; 题解&#xff1a; #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define MAX_INT_NUMBER 0x7FFFFFFEtypedef struct {int capital;int profit; } ProNode;int CompareProfit(const ProNode *a, const ProNode *b) { /* 从大到小排序 */return b->pr…

根据发生异常的汇编指令以及函数调用堆栈,从内存的角度出发,估计出问题的可能原因,确定排查方向,快速定位C++软件问题

目录 1、前言 2、初步分析dump文件 3、加载更多模块的pdb文件&#xff0c;可能能看到更多行的函数调用堆栈 4、从内存的角度去看&#xff0c;估计是访问了野指针导致的&#xff0c;沿着这个怀疑的方向快速地定位了问题 5、最后 C软件异常排查从入门到精通系列教程&#xf…

力扣OJ算法题:合并两个有序链表

—————————————————————————————————————————— 正文开始 OJ算法题&#xff1a;合并两个有序链表 思路 创建一个新的空链表&#xff08;可以用malloc优化&#xff09;和两个指针L1、L2分别指向两个链表&#xff0c;遍历两个链表&am…

Chromium 中chrome.contextMenus扩展接口实现分析c++

一、chrome.contextMenus 使用 chrome.contextMenus API 向 Google Chrome 的上下文菜单中添加项。您可以选择从右键菜单中添加的对象类型&#xff0c;例如图片、超链接和页面。 权限 contextMenus 您必须在扩展程序的清单中声明 "contextMenus" 权限&#xff0c…

R语言机器学习算法实战系列(十二)线性判别分析分类算法 (Linear Discriminant Analysis)

禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍LDA的原理LDA的步骤教程下载数据加载R包导入数据数据预处理数据描述数据切割构建模型预测测试数据评估模型模型准确性混淆矩阵模型评估指标ROC CurvePRC Curve保存模型总结优点:缺…

Ubuntu(22.04)本地部署Appsmith

Ubuntu&#xff08;22.04&#xff09;安装Appsmith 简要介绍 Appsmith 是一个开源的低代码开发平台&#xff0c;旨在帮助开发者和非开发者快速构建定制化的内部应用程序和管理工具。通过直观的拖拽界面和丰富的预配置组件&#xff0c;Appsmith 让用户无需编写大量代码即可创建…

Postman使用-基础篇

前言 本教程将结合业界广为推崇和使用的RestAPI设计典范Github API&#xff0c;详细介绍Postman接口测试工具的使用方法和实战技巧。 在开始这个教程之前&#xff0c;先聊一下为什么接口测试在现软件行业如此重要&#xff1f; 为什么我们要学习Postman&#xff1f; 现代软件…

数据轻松上云——Mbox边缘计算网关

随着工业4.0时代的到来&#xff0c;工厂数字化转型已成为提升生产效率、优化资源配置、增强企业竞争力的关键。我们凭借其先进的边缘计算网关与云平台技术&#xff0c;为工厂提供了高效、稳定的数据采集与上云解决方案。本文将为您介绍Mbox边缘计算网关如何配合明达云平台&…

基于SpringBoot微信小程序的书院预约系统【附源码】

基于SpringBoot微信小程序的书院预约系统 效果如下&#xff1a; 微信小程序首页界面 用户登录界面 书院信息界面 会议室界面 管理员登录界面 管理员主界面 用户界面 书院信息界面 会议室界面 会议室预约界面 研究背景 随着社会的快速发展&#xff0c;计算机技术的影响是全面…

SpringBoot 单元测试 - 登录认证在 Spring Boot 上的标准单元测试写法。

&#x1f449; 请投票支持这款 全新设计的脚手架 &#xff0c;让 Java 再次伟大&#xff01; 不要使用 SpringBootTest 使用 SpringBootTest 进行单元测试会启动整个 Spring Boot 容器&#xff0c;并引入整个项目的 development&test 依赖。缺点是速度慢、体积大、测试目标…

HarmonyOS Next应用开发——图像PixelMap变换

【高心星出品】 图像变换 图片处理指对PixelMap进行相关的操作&#xff0c;如获取图片信息、裁剪、缩放、偏移、旋转、翻转、设置透明度、读写像素数据等。图片处理主要包括图像变换、位图操作&#xff0c;本文介绍图像变换。 图形裁剪 // 裁剪图片 x&#xff0c;y为裁剪的起…

【element-tiptap】如何把分隔线改造成下拉框的形式?

当前的分隔线只有细横线这一种形式 但是咱们可以看一下wps中的分隔线&#xff0c;花里胡哨的 这些在wps里都需要使用快捷键打出来&#xff0c;真没找到菜单在哪里 那么这篇文章咱们就来看一下如何改造分隔线组件&#xff0c;改造成下拉框的形式&#xff0c;并且把咱们想要的分…

如何调试浏览器中的内存泄漏?

聚沙成塔每天进步一点点 本文回顾 ⭐ 专栏简介⭐ 如何调试浏览器中的内存泄漏&#xff1f;1. 什么是内存泄漏&#xff1f;2. 调试内存泄漏的工具3. 如何使用 Memory 面板进行内存调试3.1 获取内存快照&#xff08;Heap Snapshot&#xff09;获取内存快照的步骤&#xff1a;快照…

【二刷hot100】day 4

终于有时间刷刷力扣&#xff0c;求实习中。。。。 目录 1.最大子数组和 2.合并区间 3.轮转数组 4.除自身以外数组的乘积 1.最大子数组和 class Solution {public int maxSubArray(int[] nums) {//就是说可以转换为计算左边的最大值&#xff0c;加上中间的值&#xff0c…

1.6,unity动画Animator屏蔽某个部位,动画组合

动画组合 一边跑一边攻击 using System.Collections; using System.Collections.Generic; using UnityEngine;public class One : MonoBehaviour {private Animator anim;// Start is called before the first frame updatevoid Start(){anim GetComponent<Animator>();…

PPT自动化:Python如何修改PPT文字和样式!

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 文章内容 📒📝 使用 Python 修改 PPT 文本内容📝 遍历所有幻灯片和文本框📝 设置和修改文本样式📝 复制和保留文本样式⚓️ 相关链接 ⚓️📖 介绍 📖 在日常工作中,PPT 的文字内容和样式修改似乎是一项永无止境的…