如何让多线程步调一致?

前几天老板突然匆匆忙忙的过来说对账系统最近越来越慢了,能不能快速优化一下?我了解了对账系统的业务后,发现还是挺简单的,用户通过在线商城下单,会生成电子订单,保存在订单库。之后物流会生成派送单给用户发货,派送单保存在派送单库。为了防止漏送或重复派送,对账系统每天还会校验是否存在异常订单。
对账系统的处理逻辑很简单,你可以参考下面的对账系统流程图。目前的对账系统处理逻辑是,首先查询订单,然后查询派送单,然后对比订单和派送单,将差异写入差异库。
在这里插入图片描述
对账系统的代码抽象之后也很简单,核心代码如下,就是在一个单线程里循环查询订单、派送单,然后执行对账,最后写入差异库。

while(存在未对账的订单){//查询未对账订单pos = getPRders();//查询派送单dos = getDOrder();//执行对账操作diff = chech(pos,dos);//差异写入差异库save(diff);
}

利用并行优化对账系统

老板要我优化性能,那我就首先要找到这个对账系统的瓶颈所在。
目前的对账系统由于订单量和派送单量巨大,所以查询对账订单getPOrders和查询派送单getDOrders相对较慢,那有没有什么办法可以快速优化一下呢?目前对账系统是单线程执行的。图形化后是下面这个样子,对于串行化的系统性能优化,首先想到的是能否利用多线程并行处理
在这里插入图片描述所以这里你应该所以,这里你应该能够看出来这个对账系统里的瓶颈,查询未对账订单getPOrder和查询派送订单getDOrder是否可以并行处理呢?显然是可以的,因为这两个操作并没有先后顺序的依赖,这两个最耗时的操作并行执行之后,执行过程如下图所示。对比一下单线程的执行示意图,你会发现同等时间里并行执行的吞吐量近乎单线程的两倍。提升效果还是相对明显的。
在这里插入图片描述
思路有了,下面我们再来看看如何利用代码实现。在下面的代码中,我们创建了两个线程T1和T2,并行执行查询未对账订单getPOrder和查询派送订单getDOrder这两个操作。在主线程中执行对账操作check和差异写入save两个操作,不过要注意的是,主线程需要等待线程T1和T2执行完才能执行check和save两个操作。为此,我们通过调用T1.join和T2.join来实现等待。当T1和T2线程退出时。调用T1.join和T2.join的主线程就会从阻塞状态被唤醒,从而执行之后的check和save。

while(存在未对账订单){//查询未对账订单Thread T1 = new Thread(() ->{pos = getPOrder();});T1.start();//查询派送订单Thread T2 = new Thread(()->{dos = getDOrder();});T2.start();//等待T1 T2的结果T1.join();T2.join();//执行对账操作diff = check(pos,dos);save(diff);
}

用CountDownLatch实现线程等待

经过上面的优化之后,基本可以和老板汇报收工了,但是有点美中不足,相信你也发现了,while循环里面每次都会创建新的线程,而创建线程可是个耗时的操作,所以最好是创建出来的线程能够循环利用。估计这时候你已经想到线程池了,是的,线程池就能解决这个问题。
而下面的代码就是利用线程池优化之后的,我们首先创建一个固定大小为2的线程池,之后在while循环里面重复利用,一切看上去都很顺利。但是有个问题好像无解了,那就是主线程如何知道getPOrder和getDOrder这两个操作什么时候执行完?前面的主线程通过调用线程T1和T2的join方法来等待线程T1和T2退出,但是在线程池的方案里,线程根本不会退出,所以join方法已经失效了。

//创建两个线程的线程池
Executor executor = Executor.newFixedThreadPool(2);
while(存在未对账订单){//查询未对账订单executor.execute(() ->{pos = getPOrder();});//查询派送订单executor.execute(()->{dos = getDOrder();});/* 如何实现线程等待呢?*///执行对账操作diff = check(pos,dos);save(diff);
}

那如何解决这个问题呢?你可以开动脑筋想出很多办法。最直接的办法是弄一个计数器,初始值设置成2,当执行完pos = getPOrder();这个操作之后,计数器减1,执行完dos = getDOrder();之后,计数器也减1,在主线程里,等待计数器等于零,当计数器等于零时,说明这两个查询操作执行完了。等待计数器等于零其实就是一个条件变量,用管程实现起来也很简单。
不过我并不建议你在实际项目中去实现上面的方案,因为Java并发包里已经提供了实现类似功能的工具类:CountDownLatch,这里我们可以直接使用。下面的代码示例中,在while循环里面,我们首先创建了一个CountDownLatch,计数器的初始值等于2。之后在pos = getPOrder();和dos = getDOrder();两个语句的后面对计数器执行减1操作。这个对计数器减1的操作是通过调用latch.countDown()来实现的。在主线程中,我们通过调用latch.await()来实现对计数器等于0的等待。

Executor executor = Executor.newFixedThreadPool(2);
while(存在未对账订单){//计数器初始化未2CountDownLatch latch = new CountDownLatch(2);//查询未对账订单executor.execute(() ->{pos = getPOrder();latch.countDown();});//查询派送订单executor.execute(()->{dos = getDOrder();latch.countDown()});//等待连个查询操作结束latch.await();//执行对账操作diff = check(pos,dos);save(diff);
}

进一步优化性能

经过上面的重重优化之后,长出一口气,终于可以交付了。不过在交付之前还需要再次审视一番,看还有没有优化的余地,仔细看还是有的。
前面我们将getPOrder和getDOrder这两个查询操作并行了,但这两个查询操作和对账操作check save之间还是串行的。很显然,这两个查询操作和对账操作也是可以并行的。也就是说,在执行对账操作的时候,可以同时去执行下一轮的查询操作,这个过程可以形象的描述为下面这幅图。
在这里插入图片描述
那接下来我们再来思考一下如何实现这步优化,两次查询操作都能够和对账操作并行。对账操作还依赖查询操作的结果,这明显有点生产者-消费者的意思。两次查询操作是生产者,对账操作是消费者。既然是生产者-消费者模型,那就需要有个队列来保存生产者生产数据,而消费者则从这个队列消费数据。
不过针对对账这个项目,我设计了两个队列,并且两个队列的元素之间还有对应关系,具体如下图所示,查询订单查询操作将订单查询结果插入订单队列,派送单查询操作将派送单插入派送单队列,这两个队列的元素之间是有一一对应的关系的。两个队列的好处是,对账操作可以每次从订单队列出一个元素,从派送单队列出一个元素,然后对这两个元素执行对账操作,这样数据一定不会乱掉。
在这里插入图片描述
下面再来看看如何利用双队列来实现完全的并行。一个最直接的想法是,一个线程T1执行订单的查询工作,一个线程T2执行派送单的查询工作,当线程T1和T2都各自生产完一条数据的时候,就通知线程T3执行对账操作。这个想法虽然看上去很简单,但其实还隐藏着一个条件,那就是线程T1和线程T2的工作步调要一致,不能一个跑的太快,一个跑的太慢。只有这样才能做到各自生产完一条数据的时候,通知线程T3。
下面这幅图形象的描述了上面的意图,线程T1和线程T2只有都生产完了一条数据的时候,才能一起向下执行,也就是说线程T1和线程T2还要相互等待,步调要一致。同时线程T1和T2都生产完一条数据的时候,还要能够通知线程T3执行对账操作。
在这里插入图片描述

用CyclicBarrier实现线程同步

下面我们就来实现上面提到的方案,这两个方案的难点有两个,一个是线程T1和T2要做到步调一致,另一个是要能够通知到线程T3。
你依然可以利用一个计数器来解决这两个难点。计数器初始化为2,线程T1和T2生产完一条数据,都将计数器减1,如果计数器大于0,则线程T1或T2等待,如果计数器等于零,则通知线程T3,并唤醒等待的线程T1和T2。与此同时,将计数器重新置为2。这样线程T1和T2生产下一条数据的时候,就可以继续使用这个计数器了。
同样,还是建议你不要在实际项目中这么做,因为Java并发包里也已经提供了相关的工具类:CyclicBarrier。下面的代码中,我们首先创建了一个计数器初始值为2的CyclicBarrier,你需要注意的是创建CyclicBarrier的时候我们还需要传入一个回调函数,当计数器减到0的时候会调用这个回调函数。
线程T1负责查询订单,当查询出一条的时候调用barrier.await()来将计数器减1,同时等待计数器变成0。线程T2负责查询派送订单,当查询出一条时也调用barrier.await()来将计数器减1,同时等待计数器变成0,当T1和T2都调用barrier.await()的时候,计数器会减到0,当T1和T2,此时T1和T2就可以执行下一条语句了,同时还会调用的回调函数来执行对账操作。
非常值得一提的是,CyclicBarrier的计数器有自动重置功能,当减到零的时候会自动重置回你设置的初始值,这个功能看用起来实在太方便了。

//订单队列
Vector<P> pos;
//派送单队列
Vector<P> dos;
//执行回调的线程池
Executor executor = Executor.newFixedThreadPool(1);
final CyclicBarrier barrier = new CyclicBarrier(2,()->{executor.execute(() -> check());});void check(){P p = pos.remove(0);D d = dos.remove(0);//执行对账操作diff = check(p,d);save(diff);
}void checkAll(){//查询未对账订单Thread T1 = new Thread(() ->{while(存在未对账订单){//查询订单库pos.add(getPOrder());//等待barrier.await();}});T1.start();//查询派送订单Thread T2 = new Thread(() ->{while(存在未对账订单){//查询订单库pos.add(getDOrder());//等待barrier.await();}});T2.start();
}

总结

CountDownLatch和CyclicBarrier是Java并发包提供的两个非常应用的线程同步工具类,这两个工具类的用法区别在这里还是有必要再强调一下的。CountDownLatch主要用于解决一个线程等待多个线程的场景可以类比于旅游团长要等待所有的旅客到齐才能去下一个景点,而CyclicBarrier是一组线程之间互相等待,更像是几个驴友之间的不离不弃。

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

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

相关文章

Redis - 数据类型映射底层结构

简介 从数据类型上体现就是&#xff0c;同一个数据类型&#xff0c;在不同的情况下会使用不同的编码类型&#xff0c;底层所使用的的数据结构也不相同。 字符串对象 字符串对象的编码可以是 int、raw 和 embstr 三者之一。 embstr 编码是专门用于保存简短字符串的一种优化编…

网络安全--负载均衡

负载均衡 webshell实践 一、负载均衡配置 1.在全局的http下写下它&#xff1a; upstream nginx_boot{# 30s内检查心跳发送两次包&#xff0c;未回复就代表该机器宕机&#xff0c;请求分发权重比为1:2server 192.168.0.000:8080 weight100 max_fails2 fail_timeout30s; ser…

LeetCode150道面试经典题-- 合并两个有序链表(简单)

1.题目 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 2.示例 示例 1&#xff1a; 输入&#xff1a;l1 [1,2,4], l2 [1,3,4] 输出&#xff1a;[1,1,2,3,4,4] 示例 2&#xff1a; 输入&#xff1a;l1 [], l2 [] 输…

“一日之际在于晨”,欢迎莅临WAVE SUMMIT上午场:Arm 虚拟硬件早餐交流会

8月16日&#xff0c;盛夏的北京将迎来第九届WAVE SUMMIT深度学习开发者大会。在峰会主论坛正式开启前&#xff0c;让我们先用一份精美的元气早餐&#xff0c;和一场“Arm虚拟硬件交流会”&#xff0c;唤醒各位开发小伙伴的开发魂&#xff01; 8月16日&#xff0c;WAVE SUMMIT大…

时序预测 | MATLAB实现WOA-CNN-LSTM鲸鱼算法优化卷积长短期记忆神经网络时间序列预测

时序预测 | MATLAB实现WOA-CNN-LSTM鲸鱼算法优化卷积长短期记忆神经网络时间序列预测 目录 时序预测 | MATLAB实现WOA-CNN-LSTM鲸鱼算法优化卷积长短期记忆神经网络时间序列预测预测效果基本介绍模型描述程序设计学习总结参考资料 预测效果 基本介绍 时序预测 | MATLAB实现WOA-…

R语言APSIM模型进阶应用与参数优化、批量模拟实践技术

随着数字农业和智慧农业的发展&#xff0c;基于过程的农业生产系统模型在模拟作物对气候变化的响应与适应、农田管理优化、作物品种和株型筛选、农田固碳和温室气体排放等领域扮演着越来越重要的作用。APSIM (Agricultural Production Systems sIMulator)模型是世界知名的作物生…

《论文阅读14》FAST-LIO

一、论文 研究领域&#xff1a;激光雷达惯性测距框架论文&#xff1a;FAST-LIO: A Fast, Robust LiDAR-inertial Odometry Package by Tightly-Coupled Iterated Kalman Filter IEEE Robotics and Automation Letters, 2021 香港大学火星实验室 论文链接论文github 二、论文概…

LeetCode49.字母异味词分组

我一开始的思路就是用1个hashmap<Integer,List<String>>,Integer存的的是字符串所有字母ASCLL值的和&#xff0c;List里面放异位字符串&#xff0c;但是不是异位的字符串的ascll值也可能相同比如acd和abe&#xff0c;所以这个hashmap只能降低一点时间复杂度我还是要…

Vue--》打造个性化医疗服务的医院预约系统(六)

今天开始使用 vue3 + ts 搭建一个医院预约系统的前台页面,因为文章会将项目的每一个地方代码的书写都会讲解到,所以本项目会分成好几篇文章进行讲解,我会在最后一篇文章中会将项目代码开源到我的GithHub上,大家可以自行去进行下载运行,希望本文章对有帮助的朋友们能多多关…

Web APIs 第六天

正则表达式介绍语法元字符修饰符 一.正则表达式介绍 ① 简介 用来匹配字符串中字符组合的模式在JavaScript中&#xff0c;正则表达式也是对象通常用来查找&#xff0c;替换那些符合正则表达式的文本&#xff0c;许多语言都支持正则表达式 ② 使用场景 验证表单&#xff1a…

算法通关村第4关【白银】| 栈的经典算法问题

1.括号匹配问题 思路&#xff1a;将左括号压入栈中&#xff0c;遍历字符串&#xff0c;当遇到右括号就出栈&#xff0c;判断是否是匹配的一对&#xff0c;不是就返回false&#xff08;因为按照顺序所以当遇到右括号出栈一定要是匹配的&#xff09;。使用Map来简化ifelse clas…

问道管理:放量打拐什么意思?常见的放量打拐三种形态?

成交量一直是股票交易中比较重要的目标&#xff0c;那么&#xff0c;放量打拐是什么意思&#xff1f;常见的放量打拐三种形状是什么&#xff1f;下面问道管理为我们预备了相关内容&#xff0c;以供参阅。 放量打拐什么意思&#xff1f; 放量是指股票成交量与前几个交易日比较显…

安装和配置 Ansible

安装和配置 Ansible 按照下方所述&#xff0c;在控制节点 control.area12.example.com 上安装和配置 Ansible&#xff1a; 安装所需的软件包 创建名为 /home/curtis/ansible/inventory 的静态清单文件&#xff0c;以满足以下要求&#xff1a; node1 是 dev 主机组的成员 node2 …

openGauss学习笔记-43 openGauss 高级数据管理-事件触发器

文章目录 openGauss学习笔记-43 openGauss 高级数据管理-事件触发器43.1 语法格式43.2 参数说明43.3 示例 openGauss学习笔记-43 openGauss 高级数据管理-事件触发器 触发器会在指定的ddl事件发生时自动执行函数。目前事件触发器仅在PG兼容模式下可用。 43.1 语法格式 创建事…

独家!网络机顶盒哪个好?测评员深度对比盘点网络机顶盒排名

网络机顶盒称得上是家家户户必备&#xff0c;每年我都会进行网络机顶盒的测评&#xff0c;今年已经测评过十几款了&#xff0c;后台收到很多私信不知道网络机顶盒哪个好&#xff0c;我本期整理了网络机顶盒排名&#xff0c;大家在选购时可以参考&#xff1a; ◆泰捷WEBOX 60Pro…

Keepalived + Nginx 实现高可用

一、简介 浮动IP、漂移IP地址又叫做VIP&#xff0c;也就是虚拟IP。 Keepalived 是一种高性能的服务器高可用或热备解决方案。 Keepalived 可以用来防止服务器单点故障的发生&#xff0c;通过配合 Nginx 可以实现 web 前端服务的高可用。 Keepalived 以 VRRP 协议为实现基础&a…

【C++】模板template

&#x1f525;&#x1f525; 欢迎来到小林的博客&#xff01;&#xff01;       &#x1f6f0;️博客主页&#xff1a;✈️林 子       &#x1f6f0;️博客专栏&#xff1a;✈️ C       &#x1f6f0;️社区 :✈️ 进步学堂       &#x1f6f0;️欢…

Django之定时任务--apscheduler

Django--定时任务apscheduler的使用 apscheduler定时任务的使用1、安装包2、配置settings.py3、在manage.py的文件同级目录下创建文件scheduler.py4、在项目的urls.py中调用这个定时计划5、然后启动项目 python manage.py runserver,在admin中查看就能看到你的定时任务及执行的…

机器学习算法之-逻辑回归(1)

什么是回归 回归树&#xff0c;随机森林的回归&#xff0c;无一例外他们都是区别于分类算法们&#xff0c;用来处理和预测连续型标签的算法。然而逻辑回归&#xff0c;是一种名为“回归”的线性分类器&#xff0c;其本质是由线性回归变化而来的&#xff0c;一种广泛使用于分类问…

Vue 引入 Element-UI 组件库

Element-UI 官网地址&#xff1a;https://element.eleme.cn/#/zh-CN 完整引入&#xff1a;会将全部组件打包到项目中&#xff0c;导致项目过大&#xff0c;首次加载时间过长。 下载 Element-UI 一、打开项目&#xff0c;安装 Element-UI 组件库。 使用命令&#xff1a; npm …