Skip to Content
面试题库Thread Pool线程池

线程池

线程池的特点是异步执行,其使用场景是系统需要提高性能。常见的使用场景是有大量的查询业务或者是插入业务,这个时候就需要使用到线程池。

线程池基础知识

你说到Java线程池,就一定会聊到ThreadPoolExecutor,通过参数的设置,可以达到不同的效果,比如说:FixedThreadPool(固定线程池)、CachedThreadPool(缓存线程池)、ScheduledThreadPool(定时线程池)和SingleThreadExecutor(单线程池)。

但是,这些线程池大部分时候都不会使用的,因为他们多多少少是存在一些问题的,在生产上我们一般使用自ThreadPoolExecutor。

ThreadPoolExecutor 的核心特点是资源复用与并发控制:异步任务处理,利用核心线程常驻处理,利用阻塞队列缓冲请求保证高可用性。

相关阅读:

相关阅读:

线程池的类型

其实java的所有接口,都可以看作是任务。而具体我们可以将其分为三种:CPU密集型任务,IO密集型任务和混合密集型任务。

相关阅读:

所以,线程池也可以分为三种类型,CPU线程池IO型线程池、和混合线程池(这种通常不说,没啥意义)

  • CPU型线程池依赖于CPU资源执行计算任务,过多的线程反而会引发对于CPU资源的竞争。
  • IO型线程池不依赖CPU资源,只需要CPU执行线程上下文切换的任务,过多的线程可以提高整体的并发度。所以IO型线程池会在保证不乱创建线程的情况下,尽可能多地创建线程。

这两种线程池在实现上的主要区别是入队策略的不同

  • IO型线程池创建核心线程的策略是有空闲线程,就添加线程:I/O密集型任务,大部分在阻塞的等待I/O的read & write,增加线程数,提高并发度,尽可能多的处理任务。
  • CPU型线程池创建核心线程的策略是不到万不得已不创建线程:CPU密集型任务不要大量线程,原因在于线程忙于计算,创建多的线程会导致上下文的切换,降低任务的处理速度。

因为IO型侧重于阻塞,CPU型侧重于计算,换句话说,线程的状态常处于阻塞态,就是IO型,线程的状态常处于运行态,就是CPU型。

相关阅读:

线程池的监控(快手面试题)

设计监控系统的核心难点在于两点:第一是保证系统的可观测性,第二是平衡监控数据的实时性与系统开销

为了把这个架构讲透,我们可以顺着数据的流转链路,从四个核心步骤来拆解:分别是数据结构设计、数据采集、数据缓冲,以及大盘展示的实时性方案选型。

第一步:数据结构的设计(我们要看什么?)

监控的第一步是明确指标。当线上出现故障(比如任务堆积、响应变慢)时,我们需要三个维度的数据来定位问题:

  1. 身份画像数据:任务名称、线程池名称、线程 ID。(知道是谁在运行)
  2. 线程池状态数据:核心线程数、最大线程数、非核心线程数、任务队列中的排队任务数。(判断是否发生资源挤兑或任务积压)
  3. 执行性能数据:单个任务执行耗时、总耗时。(判断是否有慢任务阻塞了线程)

第二步:数据的无侵入采集(我们怎么拿到数据?)

有了指标模型,接下来是怎么采?有一个关键点是:监控逻辑绝对不能反向拖垮业务逻辑。

所以不会在业务代码里到处埋点,而是利用 ThreadPoolExecutor 原生的扩展机制。它提供了两个非常重要的钩子函数(Hook):beforeExecute 和 afterExecute。

  • 在 beforeExecute 中:我们记录任务的开始时间,打上线程标签。
  • 在 afterExecute 中:我们记录任务结束时间,计算出单个任务的执行耗时,并在这个瞬间抓取线程池的队列长度和活跃线程数。

第三步:数据的异步流转(数据怎么推出去?)

数据收集到了,如果让执行业务的线程直接去写数据库,网络 IO 必然会降低主业务的吞吐量。

所以这里必须引入经典的生产者-消费者模式来进行异步解耦

  • 业务线程就是生产者,它把收集好的监控数据,极速放进一个内存阻塞队列中(比如 ArrayBlockingQueue)。
  • 我们会在后台单独启动一个或几个守护线程作为消费者,从队列里把数据拿出来进行下一步的推送。

第四步:存储与大盘展示选型(如何解决实时性问题?)

消费者拿到数据后,要把数据喂给监控大盘。这时候,技术选型的核心就变成了 “对实时性的要求”与“系统抗压能力” 之间的折中。这里我把大盘的实时性分为强、中、弱三个级别来设计方案:

好的,现在我们来思考一件事,实时性描述的是时间跨度,那么它的本质其实是所谓的时间事件,参考Redis对时间事件的实现,在发生时间事件时,就会利用事件驱动框架,不断进行循环,检查是否有时间事件被触发了,而我们这个也是,本质上就是利用循环查询触发的事件,比如说查redis,查mysql,都是时间事件发生了,然后进行查询,而对于TCP建立的可靠连接,其本质上是IO事件。

  1. 强实时性(秒级/亚秒级延迟 —— 没必要实现这种)

    1. 特点:任何风吹草动必须立刻感知。它的本质是“事件驱动”。
    2. 方案选型:我们可以通过 TCP 长连接建立可靠通道,将数据以微批处理的方式推给大盘;或者更优雅的方式是引入消息队列(MQ)。消费者把数据打进 MQ,大盘前端通过 WebSocket 订阅后端,后端直接消费 MQ 实时推送。MQ 既能保证高吞吐,又能应对流量洪峰。
  2. 基本实时性(几秒到十几秒延迟 —— 适用于常规实时看板)

    1. 特点:不需要毫秒级响应,允许大盘以几秒钟的频率去“拉取”数据。
    2. 方案选型:使用 Redis。消费者线程把数据聚合后更新到 Redis 里。因为 Redis 抗并发能力极强,监控大盘哪怕有大量运营人员同时在疯狂刷新,Redis 也能轻松扛住高频的读取请求。
  3. 弱实时性(分钟级延迟 —— 适用于历史趋势分析、T+1报表)

    1. 特点:主要看长时间跨度内的趋势变化。
    2. 方案选型:使用 MySQL。关系型数据库扛不住高并发的实时写入和查询,所以我们的消费者线程可以在内存中做时间窗口(比如每1分钟)的聚合,然后触发定时任务,批量存入 MySQL。大盘只在拉取历史曲线时去查询它。

线程池的复用和新建的选择

它是一个选哪个答案都对的选择题,其本身没有绝对的答案,主要是看当前业务场景。

任务的紧迫度

从任务紧迫度上,决定了被执行的任务是否允许被短暂积压和延迟执行,因此一般紧迫性任务和非紧迫性任

务要分开使用不同的线程池,甚至紧迫性任务需要独占专属线程池,而非紧迫性任务可以使用共享线程池。

  • 紧迫性任务:对任务执行时间要求比较敏感,迫切希望在最短的日时间内完成任务。
  • 非紧迫性任务:对任务执行时间要求不高,只要任务最终能够完即可,不追求时效性。

任务结果的敏感度

  • 结果敏感性任务:一般结果敏感性任务也都同属于紧迫性任务。任务提交后,需要等待并获取其任务执行的结果,一般通过submit提交任务,并通过future或CompleteFuture来实现结果的异步获取。
  • 结果不敏感性任务:任务执行方法返回void,只需要任务得到执行即可,不需要反馈结果。一般通过execute提交任务。

相关阅读:

最后更新于:
⏱️

登录后开始阅读

当前 MVP 版本只开放邮箱密码登录注册。完成登录后即可进入面试题内容。

还没有账号?先去注册