「浅谈线程池」
来自 gpt 的摘要: 线程池是管理线程的资源池,主要作用在于通过重复利用已创建的线程降低资源消耗、提高响应速度、以及管理线程的使用和分配。常见的线程池参数包括核心线程数、最大线程数、任务队列、线程存活时间等。线程池在任务执行时,首先创建核心线程处理任务,当任务量超过核心线程数时,进入阻塞队列,队列满时会根据最大线程数创建新线程,超出后根据预设的拒绝策略处理多余任务。线程池的选择可以通过 ThreadPoolExecutor 或 Executors 工具类创建,常用的线程池类型包括 SingleThreadExecutor、FixedThreadPool、CachedThreadPool 和 ScheduledThreadPool。根据任务的特性,线程池的线程数可以根据 CPU 密集型任务和 I/O 密集型任务来规划,CPU 密集型任务推荐使用等于 CPU 核心数的线程,I/O 密集型任务建议使用多倍于核心数的线程数。引入线程池能有效提高系统的性能,尤其适合 I/O 密集型任务,通过并发执行减少等待时间,从而提升整体计算效率。
讲讲线程池
线程池简介
通俗来讲,线程池就是管理一系列线程的资源池,其提供了一种限制和管理线程资源的方式。每个线程池还维护一些基本统计信息,例如已完成任务的数量。
线程池的好处
资源重用:线程不断创建和销毁消耗了大量不必要的系统资源,线程池通过资源重用的特点,重复利用已创建的线程,显著降低了所带来的资源消耗,包括内存和 CPU 时间。这有助于减少系统的开销,并提高整体性能。
提高响应速度:线程创建的时间一定程度上降低了系统的响应速度,由于线程池中的线程已经预先存在,当任务到达时,它们可以立即开始执行,无需等待线程的创建过程。这大大提高了系统的响应速度,特别是在高并发场景下。
控制并发数:不合理的并发数会导致资源耗尽或响应速度下降,线程池可以有效地控制并发执行的任务数量,通过设置线程池的核心线程数、最大线程数以及队列大小等参数,可以根据系统的承载能力来限制并发级别,提高系统资源的利用率。
提高线程的可管理性:线程是稀缺资源,无限制的创建不仅会消耗系统资源,还可能降低系统的稳定性。线程池提供了统一的线程分配、调优和监控机制,使得线程的管理更加便捷和高效。
提供灵活的线程调度策略:开发者有时会需要线程进行特殊调度,线程池通常支持多种线程调度策略,如优先级队列、定时调度、阻塞队列等,这些策略可以根据任务的特性来合理分配线程资源,优化程序运行效率。
异常处理与监控:单个线程的异常有可能会造成不可控的问题,如整个应用程序崩溃。线程池能够更好地统一管理和处理线程的异常情况,并提供了监控机制,可以实时查看线程的状态和性能数据,便于进行性能分析和调优。
线程池的参数
ThreadPoolExecutor 3 个最重要的参数:
corePoolSize:任务队列未达到队列容量时,最大可以同时运行的线程数量。
maximumPoolSize :任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
workQueue:新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
ThreadPoolExecutor 其他常见参数 :
keepAliveTime:线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime 才会被回收销毁。
unit:keepAliveTime 参数的时间单位。
threadFactory:executor 创建新线程的时候会用到。
handler:拒绝策略(后面会单独详细介绍一下)。
线程池的拒绝策略
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolExecutor 定义一些策略:
ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用 execute 方法的线程中运行( run )被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
ThreadPoolExecutor.DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。
如何用线程池
方式一:通过 ThreadPoolExecutor 构造函数来创建(推荐)。
方式二:通过 Executor 框架的工具类 Executors 来创建。
常用的线程池
FixedThreadPool:固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
SingleThreadExecutor: 只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
CachedThreadPool: 可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
ScheduledThreadPool:给定的延迟后运行任务或者定期执行任务的线程池。
线程池的执行流程
当提交一个新任务到线程池时,具体的执行流程如下:
当我们提交任务,线程池会根据 corePoolSize 大小创建若干任务数量线程执行任务
当任务的数量超过 corePoolSize 数量,后续的任务将会进入阻塞队列阻塞排队
当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize - corePoolSize)个数量的线程来执行任务,如果任务处理完成,maximumPoolSize - corePoolSize 额外创建的线程等待 keepAliveTime 之后被自动销毁
如果达到 maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理

线程池的核心线程数和最大线程数如何规划
有一个简单并且适用面比较广的公式 :
CPU 密集型任务 (N): 这种任务消耗的主要是 CPU 资源,线程数应设置为 N(CPU 核心数)。由于任务主要瓶颈在于 CPU 计算能力,与核心数相等的线程数能够最大化 CPU 利用率,过多线程反而会导致竞争和上下文切换开销。
I/O 密集型任务(M * N):这类任务大部分时间处理 I/O 交互,线程在等待 I/O 时不占用 CPU。 为了充分利用 CPU 资源,线程数可以设置为 M * N,其中 N 是 CPU 核心数,M 是一个大于 1 的倍数,建议默认设置为 2 ,具体取值取决于 I/O 等待时间和任务特点,需要通过测试和监控找到最佳平衡点。
CPU 密集型任务不再推荐 N+1,原因如下:
"N+1" 的初衷是希望预留线程处理突发暂停,但实际上,处理缺页中断等情况仍然需要占用 CPU 核心。
CPU 密集场景下,CPU 始终是瓶颈,预留线程并不能凭空增加 CPU 处理能力,反而可能加剧竞争。
如何判断是 CPU 密集任务还是 IO 密集任务?
CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。
为何在项目中引入线程池?
参考:由于这个项目它主要的性能瓶颈是在于网络 IO,调用的接口平时时长为100 ms,高负载的时候高达数秒,实际获取数据的时间是大于我们数据实际计算的时间,可见这个项目是一个 IO 密集型的项目,所以我们引入线程池来进行异步计算,通过提高并发数量来尽量抵消花费在 IO 等待上的计算空闲,保证计算的高性能。
