线程池ThreadPoolExecutor详解
一、什么是线程
先来看枯燥的定义:线程是操作系统能够进行运算调度的最小单位,是进程中的实际运作单元,共享进程资源但拥有独立的执行上下文。
通俗来说,线程必须依赖于进程而存在,多个线程可以并发执行,共享进程资源,具体区别可以看下表所示:
对比维度 | 程序(Program) | 进程(Process) | 线程(Thread) |
---|---|---|---|
定义 | 静态的代码和数据集合(如 .exe 、.class 文件),未运行时存于磁盘。 |
程序的一次动态执行实例,是操作系统分配资源的基本单位。 | 进程内的一条执行路径,是程序执行的最小调度单位。 |
存在形式 | 静态文件,不占用系统资源(CPU、内存等)。 | 动态实体,占用独立的内存空间、系统资源(如文件句柄、CPU 时间片)。 | 动态实体,依附于进程存在,共享进程的资源(代码段、数据段等),仅拥有独立的栈和寄存器状态。 |
资源分配 | 无,仅存储代码和数据,未激活时不涉及资源。 | 拥有独立的地址空间、内存、文件句柄、环境变量等资源,与其他进程隔离。 | 不独立分配资源,共享所属进程的资源(如打开的文件、全局变量),仅独占栈空间和线程本地存储(TLS)。 |
调度单位 | 不参与调度(非运行态)。 | 操作系统早期的调度单位(现在多为线程),调度开销较大。 | 现代操作系统的基本调度单位,调度开销小(上下文切换更快)。 |
独立性 | 无运行概念,不依赖进程 / 线程(仅作为文件存在)。 | 独立运行,不同进程间通过 IPC(进程间通信)协作,资源互不干扰。 | 不能独立存在,必须依附于进程,依赖进程提供运行环境(如内存空间、资源句柄)。 |
并发性 | 无并发性(静态文件)。 | 可包含多个线程,通过多线程实现内部并发;也可通过多进程实现并发(需 IPC)。 | 作为进程内的执行单元,多个线程可并发执行(CPU 分时或多核并行)。 |
生命周期 | 持久存在(除非被删除)。 | 动态创建(如 fork() 、双击程序启动),动态销毁(如进程结束或崩溃)。 |
动态创建(如 pthread_create() 、Java new Thread() ),动态销毁(如线程结束或进程终止)。 |
举例 | 磁盘上的 微信.exe 、AndroidManifest.xml (未运行)。 |
运行中的微信程序(一个进程)、Android 应用进程(如 com.tencent.wechat )。 |
微信进程中的网络请求线程、UI 渲染线程、消息处理线程等。 |
本质 | “静态的指令集合” | “运行中的程序实例”(资源容器) | “进程内的执行路径”(任务单元) |
二、为什么需要线程池
今天需要讨论的是线程池,那么我们为什么需要用到线程池呢?为什么不直接通过Thread.start来创建和启动线程来执行任务呢?总结起来大概有以下3个原因
- 线程创建销毁的成本高:如果频繁处理短耗时任务(如每次请求新建一个线程),创建 / 销毁的开销可能远大于任务执行本身,导致性能瓶颈。
- 减少上下文切换开销:操作系统在调度线程时,需要保存 / 恢复线程的执行上下文(寄存器、栈指针等),大量线程频繁切换会占用大量 CPU 时间。
- 防止无限创建线程导致系统崩溃:若直接为每个任务创建新线程,当任务量突然激增(如高并发请求),可能瞬间创建成百上千个线程,耗尽内存、CPU 资源或达到系统线程数限制(如 Linux 默认线程数限制约 1 万)。
三、ThreadPoolExecutor
3.1 线程池的状态
线程池的状态 | 说明 |
---|---|
RUNNING | 允许提交并处理任务 |
SHUTDOWN | 不允许提交新的任务,但是会处理完已提交的任务 |
STOP | 不允许提交新的任务,也不会处理阻塞队列中未执行的任务,并设置正在执行的线程的中断标志位 |
TIDYING | 所有任务执行完毕,池中工作的线程数为0,等待执行terminated()的方法 |
TERMINATED | terminated()方法执行完毕 |
改变线程池状态的方法:
1 | exexutor.shutdown() // 将线程池由 RUNNING(运行状态)转换为 SHUTDOWN状态 |
注:SHUTDOWN 状态 和 STOP 状态 先会转变为 TIDYING 状态,最终都会变为 TERMINATED
获取线程池状态的方法:
1 | fixedExecutor.isShutdown // 检查执行器服务是否已经调用了 shutdown() 或者 shutdownNow() 方法。调用这两个方法之后,执行器服务会停止接受新任务,但已经提交的任务会继续执行。 |
3.2 线程池各参数的含义
定义线程池
1 | val executor = ThreadPoolExecutor( |
线程池的主要处理流程
当调用线程池execute() 方法添加一个任务时,线程池会做如下判断:
- 如果有空闲线程,则直接执行该任务;
- 如果没有空闲线程,且当前运行的线程数少于corePoolSize,则创建新的线程执行该任务;
- 如果没有空闲线程,且当前的线程数等于corePoolSize,同时阻塞队列未满,则将任务入队列,而不添加新的线程;
- 如果没有空闲线程,且阻塞队列已满,同时池中的线程数小于maximumPoolSize ,则创建新的线程执行任务;
- 如果没有空闲线程,且阻塞队列已满,同时池中的线程数等于maximumPoolSize ,则根据构造函数中的 handler 指定的策略来拒绝新的任务。
3.3 重要参数解析
KeepAliveTime
- keepAliveTime :表示空闲线程的存活时间
- TimeUnit unit :表示keepAliveTime的单位
当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
注:如果线程池设置了allowCoreThreadTimeout参数为true(默认false),那么当空闲线程超过keepaliveTime后直接停掉。(不会判断线程数是否大于corePoolSize)即:最终线程数会变为0。
workQueue 任务队列
决定了缓存任务的排队策略
ThreadPoolExecutor线程池推荐了三种等待队列,它们是:SynchronousQueue 、LinkedBlockingQueue和 ArrayBlockingQueue。
1)有界队列:
- SynchronousQueue :一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于 阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool 使用了这个队列。
- ArrayBlockingQueue:一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。一旦创建了这样的缓存区,就不能再增加其容量。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。
2)无界队列:
- LinkedBlockingQueue:基于链表结构的无界阻塞队列,它可以指定容量也可以不指定容量(实际上任何无限容量的队列/栈都是有容量的,这个容量就是Integer.MAX_VALUE)
- PriorityBlockingQueue:是一个按照优先级进行内部元素排序的无界阻塞队列。队列中的元素必须实现 Comparable 接口,这样才能通过实现compareTo()方法进行排序。优先级最高的元素将始终排在队列的头部;PriorityBlockingQueue 不会保证优先级一样的元素的排序。
注意:keepAliveTime和maximumPoolSize及BlockingQueue的类型均有关系。如果BlockingQueue是无界的,那么永远不会触发maximumPoolSize,自然keepAliveTime也就没有了意义。
threadFactory
指定创建线程的工厂。(可以不指定)
如果不指定线程工厂时,ThreadPoolExecutor 会使用ThreadPoolExecutor.defaultThreadFactory 创建线程。默认工厂创建的线程:同属于相同的线程组,具有同为 Thread.NORM_PRIORITY 的优先级,以及名为 “pool-XXX-thread-” 的线程名(XXX为创建线程时顺序序号),且创建的线程都是非守护进程。
handler 拒绝策略:
handler :表示当 workQueue 已满,且池中的线程数达到 maximumPoolSize 时,线程池拒绝添加新任务时采取的策略。(可以不指定)
策略 | 解析 |
---|---|
ThreadPoolExecutor.AbortPolicy() | 抛出 RejectedExecutionException 异常。默认策略 |
ThreadPoolExecutor.CallerRunsPolicy() | 由向线程池提交任务的线程来执行该任务 |
ThreadPoolExecutor.DiscardPolicy() | 抛弃当前的任务 |
ThreadPoolExecutor.DiscardOldestPolicy() | 抛弃最旧的任务(最先提交而没有得到执行的任务) |
最科学的的还是 AbortPolicy 提供的处理方式:抛出异常,由开发人员进行处理。