嗨大家好,我是小米,一个在Java圈里摸爬滚打了十来年的老程序员。
最近为了跳槽,刷了一波社招面试题,发现不少公司都很喜欢考多线程相关的内容。比如这道看似简单,实则暗藏玄机的题目:
「请你详细说说 ScheduledThreadPoolExecutor 的原理和应用场景。」
看到这题,我不禁想起了两年前在项目里踩过的一个坑。那时候,我们要做一个定时任务系统,我信心满满地用 ScheduledThreadPoolExecutor 写了一套,结果上线当天差点把服务器搞挂。今天咱们就来围绕这道面试题,聊聊我踩过的坑、总结的经验,还有这个类背后的玄机。
故事开场:那年我和定时任务打的交道事情要从我在上一家公司说起。那会儿我们有个需求,要定时去拉取供应商的库存数据,大概每隔 30 秒跑一次任务。
我当时写了个简单的代码:
部署上去,前两天一切正常,大家都拍手称快。结果上线第三天,服务器 CPU 突然飙到了 90%,运维小哥直接冲到我工位前:“小米,快看看你的定时任务,是不是死循环了?”
我一查日志,好家伙,果然有个任务跑飞了。
什么是 ScheduledThreadPoolExecutor?说到这,咱们得先正经介绍一下这位“罪魁祸首”:
ScheduledThreadPoolExecutor 是 JDK 自带的一个定时任务线程池,继承自 ThreadPoolExecutor,主要用来执行定时任务,或者周期性重复执行的任务。
它是 ScheduledExecutorService 接口的一个具体实现类,比起早期的 Timer,它更加稳定、灵活、线程安全。常见的创建方法:
这里的 5 是线程池的核心线程数量。
核心方法
schedule(Runnable command, long delay, TimeUnit unit)延迟一定时间执行任务。
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)固定速率周期执行,任务开始的时间间隔是固定的。
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)固定延迟周期执行,任务结束到下一个任务开始的时间间隔是固定的。
到底是哪里出问题了?当时我用的是 scheduleAtFixedRate,咱们来看看它的执行方式:
固定速率,假设第一个任务在 0 秒执行,第二个任务应该在 30 秒时执行。
如果前一个任务执行超过了 30 秒,后一个任务就会立刻开始,甚至会并发执行。
我查了日志,发现有一个供应商接口超时了,卡了 40 秒,结果线程池没等它执行完,下一个任务时间点到了,直接新开线程执行,几个周期下来,线程就堆满了。
核心线程池 5 个根本不够用,任务越堆越多,CPU 直接飙上天。
原理拆解:ScheduledThreadPoolExecutor 的内部结构1、底层结构图
2、执行流程
调用 schedule 方法时,会创建一个 ScheduledFutureTask 对象,封装任务和触发时间。
将 ScheduledFutureTask 放入一个 DelayQueue 中(这个队列是无界的,按执行时间排序)。
工作线程不断从队列里取出到期的任务,放到线程池执行。
3、DelayQueue 是什么?
DelayQueue 是一个带延迟时间的队列,只有到期的任务才能被取出。它基于 PriorityQueue 实现,队头永远是最早到期的任务。
scheduleAtFixedRate 和 scheduleWithFixedDelay 区别面试考点:
1、如果一个定时任务执行时间超过了周期时间,scheduleAtFixedRate 会怎么办?
答案:
它会尽量补上,可能导致任务并发执行。
scheduleWithFixedDelay 则永远是串行执行,前一个任务完成后才开始计算延迟。
正确用法 + 实战经验1、创建方式推荐:
推荐自己 new,别用 Executors.newScheduledThreadPool(),因为它会把核心线程数设死,容易堆积。
指定拒绝策略,防止线程过多。
2、异常处理
默认如果任务抛异常,线程会挂掉,导致线程池线程数减少,最后没人可用。
解决办法:
或者统一设置 setRemoveOnCancelPolicy(true)。
3、调整线程池参数
实时监控线程池状态,适当增减核心线程数,或者设置最大线程数。
面试必问点我总结了以下几条,面试官最爱问:
1、ScheduledThreadPoolExecutor 和 Timer 区别?
Timer 单线程,任务异常会导致整个调度停止。
ScheduledThreadPoolExecutor 多线程,异常不影响其它任务。
2、scheduleAtFixedRate 和 scheduleWithFixedDelay 区别?
3、ScheduledThreadPoolExecutor 的底层结构?
4、DelayQueue 是怎么保证任务顺序的?
5、如果线程池满了怎么办?
我的总结别看 ScheduledThreadPoolExecutor 小小一个类,细节一堆,坑也不少。
用得好,生产稳定; 用不好,分分钟上热搜。
如果你要用它做定时任务调度,记住这 5 条:
尽量 new ScheduledThreadPoolExecutor,别用 Executors 工厂方法。
用 scheduleWithFixedDelay 替代 scheduleAtFixedRate,防止任务堆积。
统一 try-catch,避免异常导致线程挂掉。
配置拒绝策略,防止 OOM。
实时监控线程池状态,灵活调整参数。
友情提示说到底,面试问这个问题,考察的不是你会不会写定时任务,而是你:
是否理解多线程的本质
能不能预测到实际运行时的问题
有没有调优和容错的意识
这才是一个成熟 Java 程序员该有的样子。
END如果你喜欢这样的面试故事+源码解析,记得关注小米!咱们下次面试题继续聊。
要是你也有类似的踩坑故事,评论区走一个,我来帮你分析分析!