线程池本质是一个(pooling)池化技术,与之相似的还有连接池 比如Mysql 对象池 StringCache。
池化的一系列好处:
1、降低资源消耗 复用了线程或者连接之类的资源,本身创建和销毁资源都是有损耗的
2、提高了响应速度 由于资源已经创建好,执行任务时无需经历创建流程
3、提高了资源的可控性 我们可以做统一的调优 监控
4、提供更强的扩张功能 比如延迟定时线程池
Executor 抽象任务提交 任务执行
ExecutorService 扩展执行任务的能力 可以为一个或多个异步任务生产Future 提供优雅停止或暴力停止的方法
AbstractExecutorService 执行任务的流程串联
ThreadPoolExecutor 实现维护自身的生命周期 管理线程和任务
首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。 1 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。 2 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。 3 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。 4 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
除了任务执行的流程 我们要需要注意
线程池运行机制:
1 线程池如何维护自身状态。 2 线程池如何管理任务。 3 线程池如何管理线程。
有一个有意思的事情 线程池里的workerCount和runState 这两个状态实际是维护在一个AtonmicInteger变量里
线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),高3位保存runState,低29位保存workerCount
任务缓冲 线程池的阻塞队列
ArrayBlockingQueue 数组实现的队列 由于数组的索引可以很快的访问可以用来实现非公平的模式
LinkedBlockingQueue 链表实现的队列 默认长度是Integer的最大值 有内存溢出的风险 是默认的使用的阻塞队列
PriorityBlockingQueue 支持线程的优先级别的无界队列 默认自然序 也可以使用CompareTo()方法来指定元素排序规则
DelayQueue 实现PriorityBlockingQueue延迟获取的无界队列 延时期满才能从队列中获取元素
SynchronousQueue 不存储元素的队列每一个put操作都必须等待take操作 支持公平 非公平锁
LinkedTransferQueue 链表结构组成的无界队列
LinkedBlockingDeque 链表结构的双向阻塞队列
任务拒绝 当线程池数达到maximumSize时,没有空闲线程,就是执行拒绝策略
jdk提供了四种拒绝策略
1、ThreadPoolExecutor.AbortPolicy 直接抛出异常
2、ThreadPoolExecutor.DiscardPolicy 直接丢弃任务
3、ThreadPoolExecutor.DiscardOldestPolicy 丢弃最老的任务
4、ThreadPoolExecutor.CallerRunsPolicy 交给提交任务的线程执行(可以减缓生产者的速率)
Worker线程的管理
Worker这个工作线程,实现了Runnable接口,并持有一个线程thread,一个初始化的任务firstTask
Worker线程的回收
Worker线程的生命周期要被管理 也就是除了创建还需要能被回收,这就涉及到收回线程和执行线程同步操作线程状态的问题。线程运行时 Worker会持有一个独占锁 用自己实现AQS来实现。
线程池的线程回收是调用processWorkerExit
1、记录任务完成个数
2、线程引用移除worker的hash表
3、线程池状态的维护
Worker线程增加
增加线程是通过线程池中的addWorker方法。addWorker方法有两个参数:firstTask、core。firstTask参数用于指定新增的线程执行的第一个任务,该参数可以为空;core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSize,false表示新增线程前需要判断当前活动线程数是否少于maximumPoolSize
Worker任务的执行
Worker类中的run方法调用了runWorker方法来执行任务。runWorker方法的执行过程:
1、while循环不断地通过getTask()方法获取任务。
2、getTask()方法从阻塞队列中取任务。
3、如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态。
4、执行任务。
5、如果getTask结果为null则跳出循环,执行processWorkerExit()方法,销毁线程。
场景分析
1、对响应时间RT的要求高。
由于对RT要求高,而对吞吐不敏感,因此应该使用队列来缓存任务,corePoolSize和maxPoolSize来尽可能创建更多的线程来快速执行任务
2、快速处理批量任务,对吞吐量有要求。 设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。
线程池的参数参考
理论上说 执行任务分为 IO密集型和CPU密集型。
CPU密集型响应时间很快,cpu一直在运行,这种任务cpu的利用率很高。线程个数为CPU核数,尽量使得所有线程都处于繁忙状态,同CPU数是为了尽量减少线程切换的开销
IO密集型执行IO操作的时间较长,这是cpu出于空闲状态,导致cpu的利用率不高. 线程个数为CPU核数的两倍。这样线程在IO操作时间较长时,其他线程会使用cpu,提高cpu利用率.
但这里的计算方式是不符合复杂场景的。
线程池参数动态化 我们需要一套能够不重启调整线程池参数的方法。从而实现快速调整 缩短故障恢复时间。我们可以将线程池的参数从代码中迁移到分布式配置中心上。实现线程池参数的动态配置,即时生效。
动态化线程池的核心设计包括以下三个方面:
简化线程池配置:线程池构造参数有8个,但是最核心的是3个:corePoolSize、maximumPoolSize,workQueue,它们最大程度地决定了线程池的任务分配和线程分配策略。考虑到在实际应用中我们获取并发性的场景主要是两种:(1)并行执行子任务,提高响应速度。这种情况下,应该使用同步队列,没有什么任务应该被缓存下来,而是应该立即执行。(2)并行执行大批次任务,提升吞吐量。这种情况下,应该使用有界队列,使用队列去缓冲大批量的任务,队列容量必须声明,防止任务无限制堆积。所以线程池只需要提供这三个关键参数的配置,并且提供两种队列的选择,就可以满足绝大多数的业务需求,Less is More。 参数可动态修改:为了解决参数不好配,修改参数成本高等问题。在Java线程池留有高扩展性的基础上,封装线程池,允许线程池监听同步外部的消息,根据消息进行修改配置。将线程池的配置放置在平台侧,允许开发同学简单的查看、修改线程池配置。 增加线程池监控:对某事物缺乏状态的观测,就对其改进无从下手。在线程池执行任务的生命周期添加监控能力,帮助开发同学了解线程池状态。