本篇文章不讨论一些线程池的基础八股,只写一些我的一些idea.
对于线程池的核心线程数,为什么一般是cpu密集型是cpu核数+1,IO密集型是cpu核数*2?
首先,线程池的核心线程数是根据任务的类型进行调整。对于cpu密集型的任务,例如大量的计算,每个线程都会占用一个cpu核心进行计算,如果线程数超过了cpu核心数,导致线程之间争夺cpu时间,那么会导致频繁的上下文切换,降低效率,所以应该是线程池核心线程数等于cpu核心数,这样子可以最大化的利用cpu而不过多切换。那么如果遇到线程因为某种原因阻塞了,如果增加一个线程核心的话,就会继续利用cpu,不会导致cpu空闲,从而保持cpu利用率。
对于IO密集型任务来说,例如文件读写,线程会在等待IO时被阻塞,这时候cpu空闲,为了更好的利用cpu,那么就可以多创建几个线程,这样子一些线程在等待IO时,另一些线程可以继续的利用cpu资源。如果是IO的时间和cpu处理的事件大致相等,那么当一半线程IO时,另一半线程可以继续的运行,这样可以使cpu保持忙碌。但是还是需要根据线程的实际使用cpu时间来订,例如线程是1/3时间在使用cpu,那么这时候可以IO密集型的线程数是cpu核心数的3倍。
四种线程池拒绝策略的共同点
首先,它们都是来处理任务被拒绝时的行为,当线程池的工作队列满了并且所有的线程(最大的线程)都在忙碌,即资源不足时,才会触发拒绝策略。它们都是为了保护线程池不被过多的任务压垮,避免资源被耗尽。是通过牺牲部分的任务来保证核心服务的可用性。
为了验证是最大核心线程和工作队列,可以使用以下代码进行验证
public class ThreadPoolBusyProof {
public static void main(String[] args) {
// 核心线程2,最大线程4,队列容量2
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
4,
30, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2),
new ThreadPoolExecutor.AbortPolicy() // 拒绝时抛异常
);
// 记录任务提交状态
AtomicInteger submittedTasks = new AtomicInteger(0);
try {
// 提交7个任务(触发拒绝)
for (int i = 1; i <= 7; i++) {
final int taskId = i;
executor.submit(() -> {
try {
System.out.printf("Task-%d START | 线程: %s | 当前活跃线程: %d | 队列大小: %d%n",
taskId, Thread.currentThread().getName(),
executor.getActiveCount(), executor.getQueue().size());
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
submittedTasks.incrementAndGet();
System.out.printf("提交Task-%d成功 | 总提交数: %d%n", taskId, submittedTasks.get());
}
} catch (RejectedExecutionException e) {
System.err.println("触发拒绝策略!当前状态 => " +
"活跃线程: " + executor.getActiveCount() +
", 队列大小: " + executor.getQueue().size() +
", 总线程数: " + executor.getPoolSize());
} finally {
executor.shutdown();
}
}
}
自定义线程池策略,并说明应用场景
可以对于任务进行优先级的排序,根据任务的优先级来决定那些任务保留,那些任务丢弃。如果当前任务的优先级高于队列的最低优先级,那么那么就移除队列中的最低优先级任务,并重新提交当前的任务,否则的话,就丢弃当前的任务。任务的优先级应该如何设计?
- 固定优先级,例如在电商的场景中,可以使vip用户订单的优先级高于普通用户订单优先级,普通用户的优先级高于促销活动日志的优先级。
- 可以长时间未完成任务的线程逐步提升优先级,防止被淘汰。
- 可以使用队列进行分级,参考操作系统的多级反馈队列将任务分到不同的队列,并根据任务的类型(cpu/io密集型)调整层级。例如可以使任务首次进入就到高优先级队列,如果一定时间内(时间片)未完成,进行降级。如果任务主动让出cpu(等待io),那么就升级到更高的优先级队列。
- 为高优先级任务分配更多的资源,使其更快速的完成。
还可以在自定义的拒绝策略中增加动态扩容线程池,临时增加线程池的最大线程数。系统偶尔遇到流量激增(如秒杀活动、定时任务触发),任务提交速率短时间内超过线程池处理能力,通过动态扩容临时提高线程池的最大线程数,快速消费积压任务,避免任务被拒绝或丢失。但是盲目增加线程数可能导致 CPU、内存等资源竞争加剧或者调整线程池参数时若未同步控制,可能引发线程池状态不一致等问题。
还可以将任务降级到外部存储,例如redis,然后由独立线程异步拉取并重新提交。应用场景:核心业务任务(如支付回调、订单状态同步)不允许丢失。或者任务需要跨多个服务或实例处理,通过共享存储实现任务分发与状态同步。但是当任务写入 Redis 成功后,如果后续处理失败(如网络中断、服务宕机),可能导致数据不一致。独立线程拉取任务后,若处理失败但未正确回滚,任务可能被多次消费。
超出核心线程数的线程底层如何回收
线程的回收并非由某个外部线程主动销毁,而是由线程自身在满足条件后主动退出执行逻辑,随后由 JVM 自动回收资源。

首先要明确,核心线程数是默认长期存活(除非开启allowCoreThreadTimeOut),只有超出核心线程数的线程才能够被回收。
回收触发的条件:
- 当前线程数大于核心线程数,就是存在非核心线程
- 线程处于空闲状态,就是没有从任务队列中获取数据
- 空闲时间超过keepAliveTime,从最后一次执行任务或者获取任务开始执行或者线程池关闭
- 线程池调用shutdown或者shutdownNow,通过interruptldleWorkers()发送中断信号,强制线程退出
回收逻辑
- 检查线程池状态,如果线程池已经关闭,那么所有的线程就直接回收,如果线程池仍在运行,检查当前的线程数是否超过核心线程数
- 从队列中获取任务,对于核心线程来说,默认调用workQueue.take(),永久阻塞等待新任务,非核心线程调用workQueue.poll(keepAliveTime,TimeUnit),限时等待新任务
- 超时判定,如果keepAliveTime内未获取到任务,触发回收逻辑,获取到了任务,重置空闲计数器,继续执行。这三条是getTask()方法里的
- 当getTask()返回null,表示线程退出runWork循环,开始进行线程回收,需要调用processWorkExit(w,completedAbruptly)方法
- processWorkExit(w,completedAbruptly)方法首先从works集合中移除改worker,再更新线程池统计信息,接下来如果满足终止条件,就会调用tryTerminate()进行线程池的终止,如果线程池仍在运行,就可能需要补充新的线程。
tryTerminate()
tryTerminate()是ThreadPoolExecutor内部用于尝试终止线程池的方法,当线程状态变为SHUTDOWN或者STOP,并且没有活跃线程时进行触发,其内部主要是将线程池转化为TERMINATED,调用terminated和唤醒可能阻塞在awaitTermination()的线程。执行逻辑:首先判断是否满足终止条件,不满足直接返回。如果有活跃线程,尝试中断一个空闲线程。如果无活跃线程,准备进入tidying状态。通过CAS操作将状态改为tidying,执行terminated()后变为TERMINATED。
1.为什么加锁
在多线程环境下,状态转换到TIDYING和TERMINATED的过程必须是原子的,不能有多个线程同时执行这个转换。若不加锁,多个线程可能同时进入此逻辑,导致多次调用terminated()钩子方法或状态混乱。同时,mainLock在ThreadPoolExecutor中通常用于保护worker集合的修改,所以在这里加锁也是为了保证在状态转换时,worker集合的状态一致。termination.signalAll() 需要与awaitTermination() 的等待线程同步,确保唤醒操作不会被其他线程干扰。
2.哪里体现CAS
代码中ctl.compareAndSet(c, ctlOf(TIDYING, 0)),使用用了CAS操作。ctl是AtomicInteger类型,保存了线程池的状态和worker数量。CAS操作在这里是为了确保只有一个线程能够成功地将状态从当前状态转换为TIDYING。如果多个线程同时尝试转换,只有第一个会成功,其他线程会因为CAS失败而继续循环。
3.terminated如何进行的
terminated是ThreadPoolExector提供的一个空实现的构造方法。允许用户在线程完成终止时执行自定义的逻辑,
allowCoreThreadTimeOut解析
它适用于需要动态调整核心线程数的场景,仅仅影响核心线程,当被设置为true且keepAliveTime<=0是抛异常,如果开启调用interruptldleWorkers进行中断所有的核心线程。
interruptldleWorkers(boolean onlyOne)首先加锁,为了保证遍历works集合(活跃线程集合)的线程安全。因为workers是HashSet类型,非线程安全,加锁为了防止并发修改导致并发修改异常(ConcurrentModificationException)。
shutdown()和shutdownNow()
两者都是ThreadPoolExecutor提供的关闭线程池的方法。
shutdown是停止接收新任务(后续提交的任务会被拒绝),继续执行已提交的任务(包括队列中等待的任务),等待所有任务完成后,关闭线程池。线程池从running变为shutdown,其中的interruptldleWorkers()是中断空闲线程。shutdown-->advanceRunState(SHUTDOWN)-->interruptldleWorkers()-->tryTerminate()
shutdownNow()停止接收新任务,尝试中断所有正在执行的任务(通过 Thread.interrupt()
),清空任务队列,返回未执行的任务列表。线程池从running或shutdown变为stop,interruptWorkers()是中断所有线程,drainQueue()清空任务队列.shutdownNow-->advanceRunState(STOP)-->interrupt-Workers()-->tryTerminate()
最后的过程
线程池workers集合移除终止的worker,接触对Thread对象的强引用(remove),之后jvm的垃圾回收器在下一次回收周期中回收该对象。JVM 会通知操作系统销毁对应的内核线程。操作系统回收线程占用的资源,例如线程占栈内存,线程描述符,寄存器状态等
为什么线程对象不会立即被回收?
- GC 机制:JVM 的垃圾回收是惰性的,仅当内存不足或达到触发条件时才会回收对象。
- Finalization:如果Thread类重写了finalization 方法,对象会先进入待清理队列,延迟回收。