线程池
线程池的特点是异步执行,其使用场景是系统需要提高性能。常见的使用场景是有大量的查询业务或者是插入业务,这个时候就需要使用到线程池。
线程池基础知识
你说到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型。
相关阅读:
线程池的监控(快手面试题)
设计监控系统的核心难点在于两点:第一是保证系统的可观测性,第二是平衡监控数据的实时性与系统开销。
为了把这个架构讲透,我们可以顺着数据的流转链路,从四个核心步骤来拆解:分别是数据结构设计、数据采集、数据缓冲,以及大盘展示的实时性方案选型。
第一步:数据结构的设计(我们要看什么?)
监控的第一步是明确指标。当线上出现故障(比如任务堆积、响应变慢)时,我们需要三个维度的数据来定位问题:
- 身份画像数据:任务名称、线程池名称、线程 ID。(知道是谁在运行)
- 线程池状态数据:核心线程数、最大线程数、非核心线程数、任务队列中的排队任务数。(判断是否发生资源挤兑或任务积压)
- 执行性能数据:单个任务执行耗时、总耗时。(判断是否有慢任务阻塞了线程)
第二步:数据的无侵入采集(我们怎么拿到数据?)
有了指标模型,接下来是怎么采?有一个关键点是:监控逻辑绝对不能反向拖垮业务逻辑。
所以不会在业务代码里到处埋点,而是利用 ThreadPoolExecutor 原生的扩展机制。它提供了两个非常重要的钩子函数(Hook):beforeExecute 和 afterExecute。
- 在 beforeExecute 中:我们记录任务的开始时间,打上线程标签。
- 在 afterExecute 中:我们记录任务结束时间,计算出单个任务的执行耗时,并在这个瞬间抓取线程池的队列长度和活跃线程数。
第三步:数据的异步流转(数据怎么推出去?)
数据收集到了,如果让执行业务的线程直接去写数据库,网络 IO 必然会降低主业务的吞吐量。
所以这里必须引入经典的生产者-消费者模式来进行异步解耦:
- 业务线程就是生产者,它把收集好的监控数据,极速放进一个内存阻塞队列中(比如 ArrayBlockingQueue)。
- 我们会在后台单独启动一个或几个守护线程作为消费者,从队列里把数据拿出来进行下一步的推送。
第四步:存储与大盘展示选型(如何解决实时性问题?)
消费者拿到数据后,要把数据喂给监控大盘。这时候,技术选型的核心就变成了 “对实时性的要求”与“系统抗压能力” 之间的折中。这里我把大盘的实时性分为强、中、弱三个级别来设计方案:
好的,现在我们来思考一件事,实时性描述的是时间跨度,那么它的本质其实是所谓的时间事件,参考Redis对时间事件的实现,在发生时间事件时,就会利用事件驱动框架,不断进行循环,检查是否有时间事件被触发了,而我们这个也是,本质上就是利用循环查询触发的事件,比如说查redis,查mysql,都是时间事件发生了,然后进行查询,而对于TCP建立的可靠连接,其本质上是IO事件。
-
强实时性(秒级/亚秒级延迟 —— 没必要实现这种)
- 特点:任何风吹草动必须立刻感知。它的本质是“事件驱动”。
- 方案选型:我们可以通过 TCP 长连接建立可靠通道,将数据以微批处理的方式推给大盘;或者更优雅的方式是引入消息队列(MQ)。消费者把数据打进 MQ,大盘前端通过 WebSocket 订阅后端,后端直接消费 MQ 实时推送。MQ 既能保证高吞吐,又能应对流量洪峰。
-
基本实时性(几秒到十几秒延迟 —— 适用于常规实时看板)
- 特点:不需要毫秒级响应,允许大盘以几秒钟的频率去“拉取”数据。
- 方案选型:使用 Redis。消费者线程把数据聚合后更新到 Redis 里。因为 Redis 抗并发能力极强,监控大盘哪怕有大量运营人员同时在疯狂刷新,Redis 也能轻松扛住高频的读取请求。
-
弱实时性(分钟级延迟 —— 适用于历史趋势分析、T+1报表)
- 特点:主要看长时间跨度内的趋势变化。
- 方案选型:使用 MySQL。关系型数据库扛不住高并发的实时写入和查询,所以我们的消费者线程可以在内存中做时间窗口(比如每1分钟)的聚合,然后触发定时任务,批量存入 MySQL。大盘只在拉取历史曲线时去查询它。
线程池的复用和新建的选择
它是一个选哪个答案都对的选择题,其本身没有绝对的答案,主要是看当前业务场景。
任务的紧迫度
从任务紧迫度上,决定了被执行的任务是否允许被短暂积压和延迟执行,因此一般紧迫性任务和非紧迫性任
务要分开使用不同的线程池,甚至紧迫性任务需要独占专属线程池,而非紧迫性任务可以使用共享线程池。
- 紧迫性任务:对任务执行时间要求比较敏感,迫切希望在最短的日时间内完成任务。
- 非紧迫性任务:对任务执行时间要求不高,只要任务最终能够完即可,不追求时效性。
任务结果的敏感度
- 结果敏感性任务:一般结果敏感性任务也都同属于紧迫性任务。任务提交后,需要等待并获取其任务执行的结果,一般通过submit提交任务,并通过future或CompleteFuture来实现结果的异步获取。
- 结果不敏感性任务:任务执行方法返回void,只需要任务得到执行即可,不需要反馈结果。一般通过execute提交任务。
相关阅读: