手撸一个多线程永动任务!
今天教大家撸一个 Java 的多线程永动任务这个示例的原型是公司自研的多线程异步任务项目我把里面涉及到多线程的代码抽离出来然后进行一定的改造。里面涉及的知识点非常多特别适合有一定工作经验的同学学习或者可以直接拿到项目中使用。文章结构非常简单1. 功能说明做这个多线程异步任务主要是因为我们有很多永动的异步任务什么是永动呢就是任务跑起来后需要一直跑下去。比如消息 Push 任务因为一直有消息过来所以需要一直去消费 DB 中的未推送消息就需要整一个 Push 的永动异步任务。我们的需求其实不难简单总结一下能同时执行多个永动的异步任务每个异步任务支持开多个线程去消费这个任务的数据支持永动异步任务的优雅关闭即关闭后需要把所有的数据消费完毕后再关闭。完成上面的需求需要注意几个点每个永动任务可以开一个线程去执行每个子任务因为需要支持并发需要用线程池控制永动任务的关闭需要通知子任务的并发线程并支持永动任务和并发子任务的优雅关闭。2. 多线程任务示例2.1 线程池对于子任务需要支持并发如果每个并发都开一个线程用完就关闭对资源消耗太大所以引入线程池public class TaskProcessUtil { // 每个任务都有自己单独的线程池 private static MapString, ExecutorService executors new ConcurrentHashMap(); // 初始化一个线程池 private static ExecutorService init(String poolName, int poolSize) { return new ThreadPoolExecutor(poolSize, poolSize, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueueRunnable(), new ThreadFactoryBuilder().setNameFormat(Pool- poolName).setDaemon(false).build(), new ThreadPoolExecutor.CallerRunsPolicy()); } // 获取线程池 public static ExecutorService getOrInitExecutors(String poolName,int poolSize) { ExecutorService executorService executors.get(poolName); if (null executorService) { synchronized (TaskProcessUtil.class) { executorService executors.get(poolName); if (null executorService) { executorService init(poolName, poolSize); executors.put(poolName, executorService); } } } return executorService; } // 回收线程资源 public static void releaseExecutors(String poolName) { ExecutorService executorService executors.remove(poolName); if (executorService ! null) { executorService.shutdown(); } } }这是一个线程池的工具类这里初始化线程池和回收线程资源很简单我们主要讨论获取线程池。获取线程池可能会存在并发情况所以需要加一个 synchronized 锁然后锁住后需要对 executorService 进行二次判空校验。2.2 单个任务为了更好讲解单个任务的实现方式我们的任务主要就是把 Cat 的数据打印出来Cat 定义如下Data Service public class Cat { private String catName; public Cat setCatName(String name) { this.catName name; return this; } }单个任务主要包括以下功能获取永动任务数据这里一般都是扫描 DB我直接就简单用 queryData() 代替。多线程执行任务需要把数据拆分成 4 份然后分别由多线程并发执行这里可以通过线程池支持永动任务优雅停机当外面通知任务需要停机需要执行完剩余任务数据并回收线程资源退出任务永动执行如果未收到停机命令任务需要一直执行下去。直接看代码public class ChildTask { private final int POOL_SIZE 3; // 线程池大小 private final int SPLIT_SIZE 4; // 数据拆分大小 private String taskName; // 接收jvm关闭信号实现优雅停机 protected volatile boolean terminal false; public ChildTask(String taskName) { this.taskName taskName; } // 程序执行入口 public void doExecute() { int i 0; while(true) { System.out.println(taskName :Cycle- i -Begin); // 获取数据 ListCat datas queryData(); // 处理数据 taskExecute(datas); System.out.println(taskName :Cycle- i -End); if (terminal) { // 只有应用关闭才会走到这里用于实现优雅的下线 break; } i; } // 回收线程池资源 TaskProcessUtil.releaseExecutors(taskName); } // 优雅停机 public void terminal() { // 关机 terminal true; System.out.println(taskName shut down); } // 处理数据 private void doProcessData(ListCat datas, CountDownLatch latch) { try { for (Cat cat : datas) { System.out.println(taskName : cat.toString() ,ThreadName: Thread.currentThread().getName()); Thread.sleep(1000L); } } catch (Exception e) { System.out.println(e.getStackTrace()); } finally { if (latch ! null) { latch.countDown(); } } } // 处理单个任务数据 private void taskExecute(ListCat sourceDatas) { if (CollectionUtils.isEmpty(sourceDatas)) { return; } // 将数据拆成4份 ListListCat splitDatas Lists.partition(sourceDatas, SPLIT_SIZE); final CountDownLatch latch new CountDownLatch(splitDatas.size()); // 并发处理拆分的数据共用一个线程池 for (final ListCat datas : splitDatas) { ExecutorService executorService TaskProcessUtil.getOrInitExecutors(taskName, POOL_SIZE); executorService.submit(new Runnable() { Override public void run() { doProcessData(datas, latch); } }); } try { latch.await(); } catch (Exception e) { System.out.println(e.getStackTrace()); } } // 获取永动任务数据 private ListCat queryData() { ListCat datas new ArrayList(); for (int i 0; i 5; i ) { datas.add(new Cat().setCatName(罗小黑 i)); } return datas; } }简单解释一下queryData用于获取数据实际应用中其实是需要把 queryData 定为抽象方法然后由各个任务实现自己的方法。doProcessData数据处理逻辑实际应用中其实是需要把 doProcessData 定为抽象方法然后由各个任务实现自己的方法。taskExecute将数据拆分成 4 份获取该任务的线程池并交给线程池并发执行然后通过 latch.await() 阻塞。当这 4 份数据都执行成功后阻塞结束该方法才返回。terminal仅用于接受停机命令这里该变量定义为 volatile所以多线程内存可见doExecute程序执行入口封装了每个任务执行的流程当 terminaltrue 时先执行完任务数据然后回收线程池最后退出。2.3 任务入口直接上代码public class LoopTask { private ListChildTask childTasks; public void initLoopTask() { childTasks new ArrayList(); childTasks.add(new ChildTask(childTask1)); childTasks.add(new ChildTask(childTask2)); for (final ChildTask childTask : childTasks) { new Thread(new Runnable() { Override public void run() { childTask.doExecute(); } }).start(); } } public void shutdownLoopTask() { if (!CollectionUtils.isEmpty(childTasks)) { for (ChildTask childTask : childTasks) { childTask.terminal(); } } } public static void main(String args[]) throws Exception{ LoopTask loopTask new LoopTask(); loopTask.initLoopTask(); Thread.sleep(5000L); loopTask.shutdownLoopTask(); } }每个任务都开一个单独的 Thread这里我初始化了 2 个永动任务分别为 childTask1 和 childTask2然后分别执行后面 Sleep 了 5 秒后再关闭任务我们可以看看是否可以按照我们的预期优雅退出。2.4 结果分析执行结果如下childTask1:Cycle-0-Begin childTask2:Cycle-0-Begin childTask1:Cat(catName罗小黑0),ThreadName:Pool-childTask1 childTask1:Cat(catName罗小黑4),ThreadName:Pool-childTask1 childTask2:Cat(catName罗小黑4),ThreadName:Pool-childTask2 childTask2:Cat(catName罗小黑0),ThreadName:Pool-childTask2 childTask1:Cat(catName罗小黑1),ThreadName:Pool-childTask1 childTask2:Cat(catName罗小黑1),ThreadName:Pool-childTask2 childTask2:Cat(catName罗小黑2),ThreadName:Pool-childTask2 childTask1:Cat(catName罗小黑2),ThreadName:Pool-childTask1 childTask2:Cat(catName罗小黑3),ThreadName:Pool-childTask2 childTask1:Cat(catName罗小黑3),ThreadName:Pool-childTask1 childTask2:Cycle-0-End childTask2:Cycle-1-Begin childTask1:Cycle-0-End childTask1:Cycle-1-Begin childTask2:Cat(catName罗小黑0),ThreadName:Pool-childTask2 childTask2:Cat(catName罗小黑4),ThreadName:Pool-childTask2 childTask1:Cat(catName罗小黑4),ThreadName:Pool-childTask1 childTask1:Cat(catName罗小黑0),ThreadName:Pool-childTask1 childTask1 shut down childTask2 shut down childTask2:Cat(catName罗小黑1),ThreadName:Pool-childTask2 childTask1:Cat(catName罗小黑1),ThreadName:Pool-childTask1 childTask1:Cat(catName罗小黑2),ThreadName:Pool-childTask1 childTask2:Cat(catName罗小黑2),ThreadName:Pool-childTask2 childTask1:Cat(catName罗小黑3),ThreadName:Pool-childTask1 childTask2:Cat(catName罗小黑3),ThreadName:Pool-childTask2 childTask1:Cycle-1-End childTask2:Cycle-1-End输出数据“Pool-childTask” 是线程池名称“childTask” 是任务名称“Cat(catName罗小黑)” 是执行的结果“childTask shut down” 是关闭标记“childTask:Cycle-X-Begin” 和“childTask:Cycle-X-End” 是每一轮循环的开始和结束标记。我们分析一下执行结果childTask1 和 childTask2 分别执行在第一轮循环中都正常输出了 5 条罗小黑数据第二轮执行过程中我启动了关闭指令这次第二轮执行没有直接停止而是先执行完任务中的数据再执行退出所以完全符合我们的优雅退出结论。

相关新闻

面试官:什么是Bean 生命周期?

面试官:什么是Bean 生命周期?

Spring Bean 的生命周期,面试时非常容易问,这不,前段时间就有个粉丝去字节面试,因为不会回答这个问题,一面都没有过。1. 基础知识1.1 什么是 IoC ?IoC,控制反转,想必大家都知道&…

2026/10/26 11:17:54 阅读更多 →
SQL 优化的 15 个小技巧,超实用!

SQL 优化的 15 个小技巧,超实用!

SQL 优化是一个大家都比较关注的热门话题,无论你在面试,还是工作中,都很有可能会遇到。如果某天你负责的某个线上接口,出现了性能问题,需要做优化。那么你首先想到的很有可能是优化 SQL 语句,因为它的改造成…

2026/10/26 11:17:52 阅读更多 →
源码深度解析,Spring 如何解决循环依赖?

源码深度解析,Spring 如何解决循环依赖?

1. 基础知识1.1 什么是循环依赖 ?一个或多个对象之间存在直接或间接的依赖关系,这种依赖关系构成一个环形调用,有下面 3 种方式。我们看一个简单的 Demo,对标“情况 2”。Service public class Louzai1 {Autowiredprivate Louzai2…

2026/10/25 13:55:38 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/10/26 12:00:21 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/10/25 22:36:10 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/26 4:52:12 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/10/26 12:00:21 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/10/26 5:00:37 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/10/26 3:16:46 阅读更多 →

日新闻

周新闻

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/26 12:00:19 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/25 13:21:54 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/26 8:34:37 阅读更多 →