参考:Java线程池实现原理及其在美团业务中的实践 - 美团技术团队 (meituan.com)
一、线程池是什么
线程池是一种对线程进行池化管理的思想和工具,广泛应用于多线程服务器中
线程的创建和销毁都会带来很多额外开销降低了服务器性能,线程池可以维护多个线程来执行任务,并且可以通过任务队列来缓存任务提高了并发能力。
线程池优点
- 降低资源消耗:池化技术可以减少线程的创建和销毁带来的性能损耗
- 提高响应速度:任务到达后可以无需等待线程创建直接执行
- 提高线程的可管理性:使用线程池可以对线程、任务进行监控和管理
- 提高更多更强大的功能:线程池具备可扩展性
二、线程池核心设计和实现
2.1 总体设计
Java中的线程池核心实现类是ThreadPollExecutor
,下图是ThreadPollExecutor
的UML类图
顶层接口Executor
提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注线程的创建、销毁、调度,用户只需要提供实现了Runnable
接口的任务对象,由Executor
框架完成线程的创建、任务的执行等。
ExecutorService
接口增加了一些能力:(1)扩充执行任务的能力,补充可以为一个或一批异步任务生成Future的方法;(2)提供了管控线程池的方法,比如停止线程池的运行。
AbstractExecutorService
则是上层的抽象类,将执行任务的流程串联起来,保证下层的实现只需要关注单个的方法实现。
ThreadPollExecutor
底层实现类
线程池的运行状态
线程池的运行状态由五种,分别为:RUNNING(运行)、SHUTDOWN(关闭,不接受新任务但会执行任务队列中的任务)、STOP(不接收新任务也不执行队列中的任务)、TIDYING(所有任务终止,有效线程数为0)、TERMINATED(终止状态)
2.2 执行机制
2.2.1 任务调度
这里涉及到几个线程池的重要参数:核心线程数
、最大线程数
、保持时间
、任务队列
、拒绝策略
-
核心线程数:核心线程数是指线程池长期保留的线程数量
-
最大线程数:线程池最大可以创建的线程个数,最大线程数大于等于核心线程数,线程池会在任务执行结束后销毁掉超出核心线程个数的线程。这部分线程的创建是作为任务队列无法容纳任务时的临时缓冲,换句话说这部分线程只会在核心线程都在执行任务并且任务队列无法容纳多余任务时才会创建出来。
-
保留时间:就是指超出核心线程数的线程执行完任务后可以存活的时间
-
任务队列:线程池的核心就是将线程和任务分离,通过线程不断从任务队列中获取任务来实现。任务队列就是用来存放任务的队列。任务队列也叫阻塞队列,因为当线程在获取任务时如果没有任务该线程会被阻塞直到获取到任务,当向队列中提交任务时如果队列已满提交任务的线程也会被阻塞直到可以提交。
常用的阻塞队列有如下几种
- ArrayBlockingQueue:数组实现的有界队列,FIFO的原则进行排序,支持公平锁和非公平锁
- LinkedBlockingQueue:链表实现的有界队列,FIFO的原则进行排序,此队列的默认最大长度为Integer.MAX_VALUE使用时注意
- PriorityBlockingQueue:支持任务优先级进行排列
- DelayQueue:延迟无界队列,任务只能在指定时间后才能被线程获取
- SynchronousQueue:不存储元素的无界队列,每个任务都必须有线程执行,否者不能继续添加元素。
-
拒绝策略:任务长时间无法被执行时将会执行拒绝策略
默认的拒绝策略有如下几种
ThreadPoolExcutor.AbortPolicy
:丢弃任务并抛出异常,也是默认的拒绝策略ThreadPoolExcutor.DiscardPolicy
:丢弃任务单不抛出异常,因为不抛出异常谨慎使用该策略ThreadPoolExcutor.DiscardOldestPolicy
:丢弃任务队列最前面的任务,然后重新提交被拒绝的策略ThreadPoolExcutor.CallerRunsPolicy
:让调用线程来执行该任务
2.2.2 Worker线程回收
线程池中线程的销毁依赖JVM的自动回收,线程池会维护线程的引用,当线程需要被销毁时,线程池只需要断开其引用即可。
三、线程池实践
3.1 业务场景
线程池主要使用在需要快速响应用户请求或者快速处理批量任务等业务场景
3.2 问题
- 工作线程设置过少会导致部分任务得不到处理
- 任务队列过长可能会导致任务堆积
3.3 解决思路
- 使用协程框架(Java中协程生态不是很好)
- 参数公式化(没有一种计算公司能适配各种业务场景)
- 参数动态化,将线程池的各种参数进行统一配置和统一管理监控(依赖于线程池框架中提供的set方法)