这篇文章,我们聊聊任务调度的极简实现:单线程永动机。
“单线程永动机”通常指某个固定线程一直执行特定任务,不会主动终止,除非外部干预(如强制终止程序或者修改运行标志位)。
1 基本编程范式单线程永动机的特点:
单线程:程序在单一线程上执行,很少涉及并发、多线程等复杂逻辑。永动机:通过无限循环(如 while (true))保持程序持续运行。控制流:为了避免占用过多系统资源,通常会使用控制流手段(如 Thread.sleep())来暂停操作。上图中,我们启动一个线程,该线程无限循环执行,每隔 20 毫秒执行业务代码。同时,我们配置了一个变量 stopped ,用于平滑关闭单线程。
在 SpringBoot 项目里,笔者一般习惯如下的编程方式:
首先定义一个容器 SingleThreadService , 内部定义 Thread 对象 ,启动方法 start 添加 PostConstruct 注解, 方法内部初始化线程,并启动。在关闭方法 shutdown 添加 PreDestroy 注解,方法内部将线程运行的标志位设置为 true 。
单线程永动机这种方式非常简单易用,在很多中间件、业务系统中得到广泛应用。
2 配置线程名我们在启动单线程时,一定要给单线程配置线程名, 线程名很重要,线程名很重要,线程名很重要 ,重要的事情说三遍。
因为当我们使用单线程时,特别担心线程阻塞,导致系统出现诡异问题。
而通过线程名,非常容易定位问题,从而大大提升解决问题的效率。
定位的媒介常见有两种:日志文件和堆栈记录。
▍一、日志文件
经常处理业务问题的同学,一定都经常与日志打交道。
查看 ERROR 日志,追溯到执行线程, 要是线程池隔离做的好,基本可以判断出哪种业务场景出了问题;通过查看线程打印的日志,推断线程调度是否正常,比如有的定时任务线程打印了开始,没有打印结束,推论当前线程可能已经挂掉或者阻塞。▍二、堆栈记录
jstack 是 java 虚拟机自带的一种堆栈跟踪工具 ,主要用来查看 Java 线程的调用堆栈,线程快照包含当前 java 虚拟机内每一条线程正在执行的方法堆栈的集合,可以用来分析线程问题。
jstack -l 进程pid3 应用场景单线程永动机适用于如下的应用场景:
单线程同步数据到静态缓存单线程批量将统计数据写入到数据库单线程监控线程,若异常,则告警举两个简单的例子:
1、加载数据到静态缓存
笔者曾经负责艺龙红包系统,红包活动信息存储在ConcurrentHashMap 中 ,通过单线程永动机刷新缓存 。
核心流程:
1、红包系统启动后,初始化一个 ConcurrentHashMap 作为红包活动缓存 ;
2、单线程永动机从数据库查询所有的红包活动 , 并将活动信息存储在 Map 中 ;
3、定时任务每隔 30 秒 ,执行缓存加载方法,刷新缓存。
2、单线程批量将统计数据写入到数据库
我们想统计博客网站的文章的浏览数,假如并发量很高,用户每浏览一次 update 一次数据库,数据库的压力会变得极大。
上图,当用户浏览文章时,我们仅仅将文章的浏览记录存储在本地缓存里,然后单线程永动机定时将统计数据同步到数据库。
通过这种方式,数据库的压力变得极小,同时用户的访问会立马返回响应成功,提升了产品的用户体验。
4 进阶版编程范式创建单线程很简单,但每次创建线程代码显得有点冗余,我们可以学习 RocketMQ 源码里的一种实现方式,它实现了一个抽象类 ServiceThread 。
我们可以看到抽象类中包含了如下核心方法:
定义线程名;启动线程;关闭线程。实现类的编程模版类似 :
我们仅仅需要继承抽象类,并实现 getServiceName 和 run 方法即可。启动的时候,调用 start 方法 , 关闭的时候调用 shutdown 方法。
在 run 方法内部,使用抽象类的 waitForRunning 方法实现等待的效果,底层是通过 CountDownLatch 的 wait 方法。
同时 ,在某些场景下,单线程可能需要与其他线程交互,ServiceThread 提供了类似于 wakeUp 唤醒方法。
5 总结1、单线程永动机的特点:
单线程:程序在单一线程上执行,很少涉及并发、多线程等复杂逻辑。永动机:通过无限循环(如 while (true))保持程序持续运行。控制流:为了避免占用过多系统资源,通常会使用控制手段(如 Thread.sleep())来暂停操作。2、我们在启动单线程时,一定要给单线程配置线程名, 线程名很重要,线程名很重要,线程名很重要 ,重要的事情说三遍。
3、笔者曾经在同程艺龙网,使用单线程永动机的模式,加载红包活动数据到本地缓存中,提升系统的性能。
4、创建单线程永动机很简单,但每次创建线程代码显得有点冗余, RocketMQ 里实现了一个抽象类 ServiceThread ,用户只需要实现 getServiceName 和 run 方法即可 , 同时抽象类 ServiceThread 还提供了 唤醒方法,在某种程度上,可以和其他线程交互。