面试官:如何保证线程池中的线程不丢失的?
我:系统能跑就行,关我咩事!
面试官:出门左拐
通常,“线程丢失”指的是以下两种糟糕的情况:
- 任务丢失:提交给线程池的任务因为各种原因(如队列满被拒绝、运行时异常)没有被执行。
- 工作线程意外终止:线程池中的工作线程(Worker Thread)因为任务抛出的未处理异常而退出死亡,导致线程池可用的线程数减少,性能下降。
线程池(ThreadPoolExecutor)通过其精巧的内部设计和一系列机制来避免这两种情况,从而保证其稳定性和可靠性。
一、如何保证【任务不丢失】?
线程池通过 “提交-处理流程” 和 “拒绝策略” 来协同保障任务不被丢失。
1. 标准任务处理流程(提交与缓冲)
线程池内部有一个标准的工作流程,就像一个高效的生产线:

这个流程通过 “队列缓冲” 机制,有效地平滑了突发流量,避免了任务被直接丢弃,是防止任务丢失的第一道防线。
2. 拒绝策略 (Rejected Execution Handler) - 最后的保障
当线程池和队列都饱和时,拒绝策略决定了如何对待新提交的任务。这是防止因资源耗尽导致系统崩溃,同时给任务一个“妥善安置”的最后机会。JDK提供了几种内置策略:
- ThreadPoolExecutor.AbortPolicy(默认策略):直接抛出 RejectedExecutionException 异常。这会让任务丢失吗? —— 会。调用方必须捕获这个异常并自行处理(如记录日志、存入数据库等待重试),否则任务就没了。
- ThreadPoolExecutor.CallerRunsPolicy:一种保证任务不丢失的重要策略。它不会抛出异常,而是让提交任务的线程自己来执行这个任务。这相当于让生产者暂时变成消费者,从而减缓任务提交速度,给予线程池喘息的机会。
- ThreadPoolExecutor.DiscardPolicy:静默地丢弃无法处理的任务,不做任何通知。会导致任务丢失。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列中最老的一个任务,然后尝试重新提交当前任务。会导致最早的任务丢失。
如何保证不丢失?
- 策略选择:如果你的任务绝对不允许丢失,可以选择 CallerRunsPolicy。虽然会影响提交任务的线程(通常是Tomcat的处理线程),但它是一种无损的策略。
- 自定义策略:你可以实现 RejectedExecutionHandler 接口,自定义拒绝逻辑。这是保证任务不丢失的最可靠方法。
- 例如:将无法处理的任务持久化到数据库、磁盘或写入Kafka/RocketMQ消息队列,并启动一个后台补偿任务,等待线程池空闲时再重新拉取执行。
// 自定义拒绝策略:将任务存入数据库,等待重试
public class DatabaseBackupPolicy implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 1. 将任务r的信息(比如实现序列化)保存到数据库
saveTaskToDatabase(r);
// 2. 记录日志,告警等
log.warn("线程池饱和,任务已持久化到数据库,等待恢复后处理");
}
}
// 使用自定义策略创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, 50, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new DatabaseBackupPolicy() // 使用自定义的拒绝策略
);
二、如何保证【工作线程不意外终止】?
这是线程池另一个非常智能的设计。关键在于理解工作线程(Worker)的 run() 方法。
线程池中的每个工作线程,并不是直接执行你提交的 Runnable 任务。它执行的是一个封装后的 Worker 对象的 run() 方法。这个方法内部有一个循环,不断地从任务队列中获取任务来执行。
最重要的是,这个循环被一个 try-catch 块包裹着。
// 线程池中Worker工作线程的大致逻辑
public void run() {
try {
while (task != null || (task = getTask()) != null) { // 循环获取任务
try {
task.run(); // 执行我们提交的Runnable任务
} catch (RuntimeException x) {
// 【重点】捕获任务执行时抛出的异常!
// 只是记录日志,不会导致循环退出,线程不会死亡!
log.error("Task execution failed.", x);
} catch (Error e) {
// 对于Error,通常选择向上抛出,线程会终止
throw e;
} finally {
task = null;
}
}
} finally {
// 线程退出前的处理...
}
}
机制说明:
如果你的任务中抛出了未检查的异常(RuntimeException),这个异常会被Worker线程捕获,只会打印日志,而不会导致Worker线程退出。Worker线程在处理完异常后,会继续循环,从队列中获取下一个任务。这就是线程不会“丢失”的核心原因。
只有当抛出了 Error 这样的严重错误时,线程才会终止。
结论: 由于线程池对工作线程的异常处理机制,单个任务的失败不会影响到执行它的工作线程。工作线程会保持存活并继续处理后续的任务,从而保证了线程池的稳定线程数量。
总结:线程池如何保证线程不丢失
1.防止任务丢失 (Preventing Task Loss):
- 队列缓冲:通过阻塞队列缓存来不及处理的任务,平滑流量峰值。
- 拒绝策略:通过合理的拒绝策略(如 CallerRunsPolicy 或自定义持久化策略)作为最后防线,确保没有任务被无声地丢弃。
2.防止工作线程终止 (Preventing Worker Thread Termination):
- 内部异常捕获:工作线程的运行循环体包裹了 try-catch,会捕获任务执行时抛出的 RuntimeException 并仅做日志记录,而不会导致工作线程意外退出,从而保证了线程池内线程数量的稳定。
在项目(如薪酬批量计算)中如何应用?
你可以这样说:“在我使用XXL-JOB进行分布式任务调度时,我配置了线程池的拒绝策略为 CallerRunsPolicy,保证在任务洪峰时不会丢弃任何一个计算任务。同时,得益于线程池自身良好的异常处理机制,单个薪酬计算任务的失败不会导致整个线程池的工作线程挂掉,从而保证了批量计算任务的持续执行能力。”
线程池如何防止线程丢失?
1400

被折叠的 条评论
为什么被折叠?



