前言

目录如下:

在面试过程中聊到并发相关的内容时,不少面试官都喜欢问这类问题:

当 N 个线程同时完成某项任务时,如何知道他们都已经执行完毕了。

这也是本次讨论的话题之一,所以本篇为『并发包入坑指北』的第二篇;来聊聊常见的并发工具。

自己实现

其实这类问题的核心论点都是:如何在一个线程中得知其他线程是否执行完毕。

假设现在有 3 个线程在运行,需要在主线程中得知他们的运行结果;可以分为以下几步:

  • 定义一个计数器为 3。
  • 每个线程完成任务后计数减一。
  • 一旦计数器减为 0 则通知等待的线程。

所以也很容易想到可以利用等待通知机制来实现,和上文的『并发包入坑指北』之阻塞队列的类似。

按照这个思路自定义了一个 MultipleThreadCountDownKit 工具,构造函数如下:

考虑到并发的前提,这个计数器自然需要保证线程安全,所以采用了 AtomicInteger

所以在初始化时需要根据线程数量来构建对象。

计数器减一

当其中一个业务线程完成后需要将这个计数器减一,直到减为0为止。

    /*** 线程完成后计数 -1*/public void countDown(){if (counter.get() <= 0){return;}int count = this.counter.decrementAndGet();if (count < 0){throw new RuntimeException("concurrent error") ;}if (count == 0){synchronized (notify){notify.notify();}}}
复制代码

利用 counter.decrementAndGet() 来保证多线程的原子性,当减为 0 时则利用等待通知机制来 notify 其他线程。

等待所有线程完成

而需要知道业务线程执行完毕的其他线程则需要在未完成之前一直处于等待状态,直到上文提到的在计数器变为 0 时得到通知。

    /*** 等待所有的线程完成* @throws InterruptedException*/public void await() throws InterruptedException {synchronized (notify){while (counter.get() > 0){notify.wait();}if (notifyListen != null){notifyListen.notifyListen();}}}
复制代码

原理也很简单,一旦计数器还存在时则会利用 notify 对象进行等待,直到被业务线程唤醒。

同时这里新增了一个通知接口可以自定义实现唤醒后的一些业务逻辑,后文会做演示。

并发测试

主要就是这两个函数,下面来做一个演示。

  • 初始化了三个计数器的并发工具 MultipleThreadCountDownKit
  • 创建了三个线程分别执行业务逻辑,完毕后执行 countDown()
  • 线程 3 休眠了 2s 用于模拟业务耗时。
  • 主线程执行 await() 等待他们三个线程执行完毕。

通过执行结果可以看出主线程会等待最后一个线程完成后才会退出;从而达到了主线程等待其余线程的效果。

    MultipleThreadCountDownKit multipleThreadKit = new MultipleThreadCountDownKit(3);multipleThreadKit.setNotify(() -> LOGGER.info("三个线程完成了任务"));
复制代码

也可以在初始化的时候指定一个回调接口,用于接收业务线程执行完毕后的通知。

当然和在主线程中执行这段逻辑效果是一样的(和执行 await() 方法处于同一个线程)。

CountDownLatch

当然我们自己实现的代码没有经过大量生产环境的验证,所以主要的目的还是尝试窥探官方的实现原理。

所以我们现在来看看 juc 下的 CountDownLatch 是如何实现的。

通过构造函数会发现有一个 内部类 Sync,他是继承于 AbstractQueuedSynchronizer ;这是 Java 并发包中的基础框架,都可以单独拿来讲了,所以这次重点不是它,今后我们再着重介绍。

这里就可以把他简单理解为提供了和上文类似的一个计数器及线程通知工具就行了。

countDown

其实他的核心逻辑和我们自己实现的区别不大。

    public void countDown() {sync.releaseShared(1);}public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {doReleaseShared();return true;}return false;}
复制代码

利用这个内部类的 releaseShared 方法,我们可以理解为他想要将计数器减一。

看到这里有没有似曾相识的感觉。

没错,在 JDK1.7 中的 AtomicInteger 自减就是这样实现的(利用 CAS 保证了线程安全)。

只是一旦计数器减为 0 时则会执行 doReleaseShared 唤醒其他的线程。

这里我们只需要关心红框部分(其他的暂时不用关心,这里涉及到了 AQS 中的队列相关),最终会调用 LockSupport.unpark 来唤醒线程;就相当于上文调用 object.notify()

所以其实本质上还是相同的。

await

其中的 await() 也是借用 Sync 对象的方法实现的。

    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;}
复制代码

一旦还存在未完成的线程时,则会调用 doAcquireSharedInterruptibly 进入阻塞状态。

    private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();}
复制代码

同样的由于这也是 AQS 中的方法,我们只需要关心红框部分;其实最终就是调用了 LockSupport.park 方法,也就相当于执行了 object.wait()

  • 所有的业务线程执行完毕后会在计数器减为 0 时调用 LockSupport.unpark 来唤醒线程。
  • 等待线程一旦计数器 > 0 时则会利用 LockSupport.park 来等待唤醒。

这样整个流程也就串起来了,它的使用方法也和上文的类似。

就不做过多介绍了。

实际案例

同样的来看一个实际案例。

在上一篇《一次分表踩坑实践的探讨》提到了对于全表扫描的情况下,需要利用多线程来提高查询效率。

比如我们这里分为了 64 张表,计划利用 8 个线程来分别处理这些表的数据,伪代码如下:

CountDownLatch count = new CountDownLatch(64);
ConcurrentHashMap total = new ConcurrentHashMap();
for(Integer i=0;i<=63;i++){executor.execute(new Runnable(){@Overridepublic void run(){List value = queryTable(i);total.put(value,NULL);count.countDown();}}) ;}count.await();
System.out.println("查询完毕");
复制代码

这样就可以实现所有数据都查询完毕后再做统一汇总;代码挺简单,也好理解(当然也可以使用线程池的 API)。

总结

CountDownLatch 算是 juc 中一个高频使用的工具,学会和理解他的使用会帮助我们更容易编写并发应用。

文中涉及到的源码:

github.com/crossoverJi…

你的点赞与分享是对我最大的支持

『并发包入坑指北』之向大佬汇报任务相关推荐

  1. 『并发包入坑指北』之阻塞队列

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 题外话 前几天我在公众号后天收到一位读者的留言: 我相信也有其他朋 ...

  2. Serverless实战 —— 三分钟入坑指北 Docsify + Serverless Framework 快速创建个人博客系统

    三分钟入坑指北

  3. 可视化拖拽生成小程序,傻瓜式免开发实现一款属于自己的小程序,云开发低码入坑指北

    前两天无意间知道了一种可以免开发,傻瓜式的拖拽生成小程序的一种方式.瞬间感觉作为程序员的石头哥离失业不远了.... 好在石头哥是一个爱学习的石头.所以今天就来看看这种靠拖拽生成小程序的平台是何方神圣. ...

  4. 美团·北极星开发对接避坑指北(Java)

    背景:公司做美团洗涤上门业务,本来好好的,后面美团要求接入美团·北极星订单预约接口,才能正常使用,没办法只能去接接口了,但是遇到了无数的坑,所以就有了这篇<美团·北极星开发对接避坑指北>, ...

  5. Android平台MediaCodec避坑指北

    https://www.jianshu.com/p/5d62a3cf0741 最近使用MediaCodec做编解码H264,写一点东西以免自己再次掉坑. 先说一下具体环境,使用的是,Windows10 ...

  6. 【Linux部署】Linux环境 .rar 格式文件处理工具安装使用(一波两折避坑指北)

    1.说明 要安装一个.rar格式的应用,上传 Linux 系统后发现没有解压工具,上网搜索后开始一波三折的旅程. 2.安装 2.1 跳坑 # 有小伙伴分享安装方法 yum install rar # ...

  7. 擦亮慧眼——找工作避坑指北!

    当今之计,靠谱的公司很多,但适合自己且靠谱的公司不一定多. 从找对象的角度换个思路看找工作. 一.动机要淳朴 找对象我们首先动机要淳朴,"不以结婚为目的的谈恋爱就是耍流氓"说的也大 ...

  8. Android环信爬坑指北(二)头像昵称好友备注显示

      在上一篇文章中提到了要在初始化的时候,设置用户信息提供者类--EaseUserProfileProvider,用以获取用户信息.下面我们来看一下 EaseUserProfileProvider 是 ...

  9. PaddlePaddle踩坑指北系列——Linux安装(一)

    本周我们精选出社区问答进行整理汇总,开发者在使用PaddlePaddle过程中遇到任何技术难题,都可以到PaddlePaddle公众号FAQ专栏上寻求解决方案,希望能帮助新用户在Linux安装过程中解 ...

最新文章

  1. Saiku二次开发获取源代码在本地编译(五)
  2. filewriter判断是否关闭_警示丨小伙用打火机检测煤气罐是否泄漏,瞬间被火焰吞噬!...
  3. rsync+sersync实现数据文件实时同步
  4. python 3.10 新增 switch-case 简介
  5. 工具使用 - Quartus II 管脚分配方法
  6. java多线程爬虫_Java 多线程爬虫及分布式爬虫架构
  7. 腾讯管家去除桌面快捷小图标
  8. 【POJ - 2785】4 Values whose Sum is 0 (二分,折半枚举)
  9. 利用光学流跟踪关键点---30
  10. linux 获取命令行返回的数据_Linux | 活用CLI命令行进行数据处理与探索
  11. 组合数学 —— 卡特兰数列(Catalan)
  12. 进制转换 [2008年北京大学图形实验室计算机研究生机试真题]
  13. 由1 2 3 4可以组成多个十百 无重复的数字
  14. 微软一个关于team editon vs 2005文章的集锦页面
  15. java点击菜单项弹出对话框_java怎么通过点击菜单弹出对话框
  16. chrome 41 空格 nbsp;
  17. linux ping 某个端口,linux 怎么ping 端口
  18. 【Python练习】乌龟吃鱼小游戏
  19. 用文件保存游戏服务器数据恢复,免越狱 教你恢复游戏数据存档
  20. CISSP考点拾遗——介质净化

热门文章

  1. 006.gcc编译器
  2. 交叉学习验证 西瓜书_机器学习:数据划分与交叉验证
  3. 十字连接焊盘_PCB板上的那些“特殊焊盘 “到底起什么作用?
  4. php intval和ceil,php取整函数ceil,floor,round,intval的区别
  5. java中10个用户注册_JavaWeb(十)Session
  6. php 如果则,如果python脚本在phpfi中运行,则导入python模块时会出错
  7. Java题目筛选器_【024期】JavaWeb面试题(五):Filter和Listener
  8. 体重 年龄 性别 身高 预测鞋码_孩子身高低于同龄人就说明发育迟缓?这个简单公式可以算出来...
  9. visio 图标_visio有效提升工作效率
  10. Ubuntu 安装 Apache Airflow