如何让多线程步调一致?

news2025/1/18 20:57:26

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

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(存在未对账订单){
	//计数器初始化未2
	CountDownLatch 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.coloradmin.cn/o/896068.html

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

相关文章

免费写真软件让你畅享写真之旅

佳媛: 嗨&#xff0c;妙纯&#xff0c;听说你喜欢拍摄写真照&#xff0c;那你有什么简单的写真照制作方式推荐吗&#xff1f; 妙纯&#xff1a;当然&#xff0c;现在有很多古装写真软件&#xff0c;你可以利用这些软件来制作写真照片哦。 佳媛: 哦&#xff1f;那拍古装写真软…

Apache JMeter

下载 Apache JMeter 并安装 java链接 打开 apache-jmeter-5.4.1\bin 找到jmeter.bat 双击打开 或者 ApacheJMeter.jar 双击打开 设置中文 找到 options 》choose Language 》chinese 新建 计划 创建线程组 添加Http请求 配置元件添加请求头参数&#xff08;content-type&…

LVS - DR

LVS-DR 数据流向 客户端发送请求到 Director Server&#xff08;负载均衡器&#xff09;&#xff0c;请求的数据报文&#xff08;源 IP 是 CIP,目标 IP 是 VIP&#xff09;到达内核空间。Director Server 和 Real Server 在同一个网络中&#xff0c;数据通过二层数据链路层来传…

miniconda克隆arcpy

arcpy环境克隆 前言尝试思考到此结束 前言 最近遇到了一些问题&#xff0c;需要用到arcpy来处理一些东西&#xff0c;但众所周知&#xff0c;arcgis的arcpy是python 2.0的&#xff0c;我不是很喜欢&#xff1b;所以我安装了arcgis pro 2.8&#xff0c;我发现这也是个坑&#x…

Redis高可用:主从复制详解

目录 1.什么是主从复制&#xff1f; 2.优势 3.主从复制的原理 4.全量复制和增量复制 4.1 全量复制 4.2 增量复制 5.相关问题总结 5.1 当主服务器不进行持久化时复制的安全性 5.2 为什么主从全量复制使用RDB而不使用AOF&#xff1f; 5.3 为什么还有无磁盘复制模式&#xff…

【学习FreeRTOS】第10章——FreeRTOS时间片调度

1.时间片调度简介&#xff08;同第2章1.3&#xff09; 同等优先级任务轮流地享有相同的 CPU 时间(可设置)&#xff0c; 叫时间片&#xff0c;在FreeRTOS中&#xff0c;一个时间片就等于SysTick 中断周期 首先Task1运行完一个时间片后&#xff0c;切换至Task2运行Task2运行完…

从SaaS到RPA,没有真正“完美”的解决方案!

众所周知&#xff0c;SaaS行业越来越卷&#xff0c;利润也越来越“薄”&#xff0c;这是传统软件厂商的悲哀&#xff0c;也是未来数字化行业不得不面对的冷峻现状之一。 随着基于aPaaS、低代码的解决方案之流行&#xff0c;SaaS行业变得越来越没有技术门槛&#xff0c;IT人员的…

Vue3.X 创建简单项目

一、环境安装与检查 首先&#xff0c;我们要确保我们安装了构建vue框架的环境&#xff0c;不会安装的请自行百度&#xff0c;有很多安装教程。检查环境 node -v # 如果没有安装nodejs请安装&#xff0c;安装教程自行百度 vue -V# 没有安装&#xff0c;请执行npm install -g v…

数据挖掘 | 零代码采集房源数据,支持自动翻页、数据排重等

1 前言 城市规划、商业选址等应用场景中经常会对地区房价、地域价值进行数据分析&#xff0c;其中地区楼盘房价是分析数据中重要的信息参考点&#xff0c;一些互联网网站上汇聚了大量房源信息&#xff0c;通过收集此类数据&#xff0c;能够对地区房价的分析提供参考依据。 如何…

ld链接文件和startup文件分析和优化--基于RT1176

ld链接文件关系到程序的代码段数据段bss段及其用户自定义段的运行位置&#xff0c;ld文件中的各个段都会在main函数之前&#xff0c;从加载域拷贝到运行域中。本章将具体介绍如何修改ld和startup文件。 软件平台&#xff1a;VSCODEGCC工具链 硬件平台&#xff1a;rt1176开发板…

如何在HTML中使用React

突发奇想 查了查真的可以,官方文档: 在网站中添加 React – React 开始 引入js <!-- 开发环境使用 --><script src"https://unpkg.com/react18/umd/react.development.js"></script><script src"https://unpkg.com/react-dom18/umd/reac…

ROS局部路径规划器插件teb_local_planner流程梳理(下)

在我之前的文章《ROS导航包Navigation中的 Movebase节点路径规划相关流程梳理》中已经介绍过Move_base节点调用局部路径规划器插件的接口函数是computeVelocityCommands&#xff0c;本部分来&#xff0c;我们从这个函数入手梳理teb_local_planner功能包的工作流程。 ☆注&#…

进入银行科技部半年,已经丧失跳槽的能力了

大家好&#xff0c;我是锋哥!&#xff01; 学弟分享 我是一个杭州双非的本科生&#xff0c;2022届毕业之后进了某银行的科技部工作&#xff0c;年包 20w。 当时想着在银行也算是一份安稳的工作&#xff0c;因此选择了给钱最多的一个&#xff0c;想着自己走上了金融 科技的赛…

Compose - 修饰符 Modifier

一、概念 四大使用场景&#xff1a; 修改外观&#xff08;尺寸、样式、布局、行为&#xff09;。添加额外信息&#xff08;如无障碍标签&#xff09;。添加交互功能&#xff08;点击、滚动、拖拽、缩放&#xff09;。处理用户输入。 1.1 为组合函数添加 Modifier 参数 任何一…

Linux网络编程:网络基础

文章目录&#xff1a; 1.协议 2.锁 3.网络层次模型 4.以太网帧和ARP协议 5.IP协议 6.UDP协议 7.TCP协议 8.BS模式和CS模式 9.网络套接字(socket) 10.网络字节序 11.IP地址转换函数 12.sockaddr地址结构 学习Linux的网络编程原则上基于&#xff1a;Linux的系统编程…

中大许少辉博士《乡村振兴战略下传统村落文化旅游设计》中国建筑工业出版社八一付梓。

中大许少辉博士《乡村振兴战略下传统村落文化旅游设计》中国建筑工业出版社八一付梓。

gdb调试的经验基本流程处理

一、启动调试 1、gdb启动 gdb启动非常简单&#xff0c;只要直接执行下面的命令&#xff1a; gdb exename(调试文件的名称)2、设置参数 如果需要调试的程序需要输入参数怎么办呢&#xff1f;有三种方法可以实现&#xff1a; a、在启动调试程序时使用命令参数设置 gdb --args …

Linux中启动docker 出现 ‘ Failed to start docker.service: Unit not found. ’ 错误

启动docker 出现 ‘ Failed to start docker.service: Unit not found. ’ 错误 这是因为缺少 rhel-push-plugin.socket 单元&#xff0c;该单元是rhel-push-plugin软件包的一部分。所以我们执行以下指令就可以成功解决&#xff1a; curl -sSL https://get.docker.com/ | sh 执…

搭建redis集群

前言 redis 集群分为一下几种&#xff1a; 【主从模式】&#xff1a;一般情况大多都是读多写少的情况&#xff0c;主从模式可以将读写分离&#xff0c;主库写&#xff0c;从库只负责读取的情况&#xff0c;这从如果任何一个从库宕机的情况&#xff0c;整个集群仍然可以提供工作…

使用GUI Guider工具开发嵌入式GUI应用(6)-切换多screen换场景

使用GUI Guider工具开发嵌入式GUI应用&#xff08;6&#xff09;-切换多screen换场景 本节将展示使用GUI Guider实现切换显示页面功能。 这里设计的用例是&#xff1a; 创建3张页面&#xff0c;screen_0,screen_1和screen_2。分别在每个页面上中放置一个Label&#xff08;最…