什么是线程池?
线程池主要用于多线程的服务器例如MYSQL中,它是基于池化思想管理线程的工具。
线程过多会带来不必要的开销,比如创建销毁线程,调度线程带来的开销,这些开销会极大的占用计算机的资源,造成计算机的整体性能的下降.线程池维护多个线程,等待监督管理者下发任务,这样既避免了处理接收到的任务时创建销毁线程带来的开销,也避免了线程数量过大可能造成的调度问题,保证了对内核的充分利用。
使用线程池带来的好处:
- 减少资源消耗:可以重复利用线程池中已经创建好的线程,避免了创建销毁线程所产生的消耗。
- 加快了响应速度:任务抵达时,减免了原本需要创建线程所占用的时间
- 提高了线程的可管理性:线程是稀缺资源,如果无限创建不仅会消耗系统资源,还会因为不合理的分布而导致系统资源失衡,降低系统的稳定性,使用线程池可以统一进行分配和调度
- 可以拓展功能:线程池具有可拓展性,可以通过线程池来实现定时任务或者延迟任务执行。
线程池主要解决的问题:
线程池归根结底要解决的最主要的是资源管理问题,在并发的环境下,系统不确定有多少线程需要执行,多少资源需要投入,这种不确定会导致若干问题:
- 频繁申请销毁调度资源,带来的额外消耗是额外巨大的。
- 系统内部无法有效管理,降低了系统本身的稳定性
- 系统内部资源申请缺乏有效的拒绝手段,有系统资源耗尽的可能性
线程池的总体设计
Java中线程池的核心实现类主要是threadpoolexcoutorexecutor,Threadpoolexecutor实现的顶层接口为executor,顶层接口提供了一种思想,将任务执行和任务提交进行解耦,用户无需关心如何创建线程,也无需关心如何调度线程执行任务,用户只需提供Runable对象,将任务的运行逻辑提交到executor中,由executor来完成线程的调度和任务的执行部分。ExecutorService接口增加了一些能力:
- 执行任务的能力,即生成一个一批异步任务生成future的方法
- 提供了管控线程池的方法,比如线程池的停止。
AbstractExecutor则是上层的抽象类,将执行任务的流程串联起来,使得下层的实现只需要关注一个执行方法即可.最下层的实现类ThreadPoolExecutor实现最复杂的运行部分,threadpoolexecutor一方面维护自身的运行另一方面管理线程和任务,从而使两者很好地结合执行并行任务。
线程池在内部实际构建了一个生产者消费者模型,线程池并没有采用将任务和线程关联的方式,而是将两者解耦,这样很好地缓冲任务,复用线程,,线程池的运行主要分为两部分:任务管理,线程管理。任务管理充当生产者角色,当任务提交后,任务管理会判断该任务后续的流转:
- 直接申请线程执行该任务
- 缓冲到队列中等待线程执行
- 拒绝该任务
线程管理担任的是消费者角色,被统一管理在线程池中,根据任务请求来进行线程的分配,当任务执行完毕后获取新的任务进行执行,当获取不到任务时会进行线程的回收。
生命周期的管理
线程池运行的状态不是有用户显式设计的,而是伴随着线程池的运行内部进行维护的。在线程池的内部使用两个变量维护一个值:运行状态(runstate),运行数量(workercount)。在具体实现中,线程池将两个状态放在一起维护:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
atomicinteger类型是对线程池的运行状态和线程池中有效运行线程数进行管理一个字段,它同时包含两部分信息,高三位保存运行状态runstate,低29位保存有效运行线程数,两个变量互不干预,同时有效避免了在做相关决策时不一致的情况,不必为了维护两个变量的一致而占用锁资源。
关于内部封装获取线程状态和数量的代码如下:
private static int runStateOf(int c) { return c & ~CAPACITY; } //计算当前运行状态
private static int workerCountOf(int c) { return c & CAPACITY; } //计算当前线程数量
private static int ctlOf(int rs, int wc) { return rs | wc; } //通过状态和线程数生成ctl
threadpoolexecutor的运行状态有五种:
- RUNNING:运行时状态,能接受新提交的任务,也能够处理阻塞队列的任务。
- SHUTDOWN:关闭状态,不再接受新提交的任务,却能够处理阻塞队列的任务。
- STOP:不接受新的任务,也不从阻塞队列中挑选任务,还会中断正在执行任务的线程。
- TIYDING:所有任务都停止了,workcount为0.
- Terminated:在Terminated方法执行完后进入此状态
任务执行机制
任务调度
任务调度实际上是线程池的主要入口,当用户提交一个新任务,,那么接下来该任务如何调度都是由该阶段来决定的.理解了这部分相当于理解了线程池的核心机制.
所有的任务调度都是由execute来决定的,这部分完成的工作是:检查运行状态,运行线程数,可执行的策略,决定接下来的流程,是直接申请线程还是缓冲队列执行,或是直接拒绝该任务,执行过程如下:
- 首先检测线程池的运行状态,若非RUNNING则直接拒绝,线程池要保证在running的状态下运行.
- 若workcount<corepoolsize,则直接创建启动线程来执行新任务.
- 若workcount>=corepoolsize,且阻塞队列未满则将任务添加到该阻塞队列中.
- 若workcount>=corepoolsize&&workcount<maxnumpoolsize且线程池在阻塞队列已满,则创建启动一个新线程来执行新提交的任务.
- 若workcount>maxmumSize,并且阻塞队列已满,则直接拒绝执行该任务,默认的处理方法为抛出异常。
任务拒绝
任务拒绝模块是线程池的保护部分,线程池有一个最大容量,当线程池的任务缓存队列已满,且线程池中的线程数达到maxnumpoolsize时,就需要拒绝掉该任务,采取拒绝策略,保护线程池。
拒绝策略其实是一个接口:
private static int runStateOf(int c) { return c & ~CAPACITY; } //计算当前运行状态
private static int workerCountOf(int c) { return c & CAPACITY; } //计算当前线程数量
private static int ctlOf(int rs, int wc) { return rs | wc; } //通过状态和线程数生成ctl
用户可以通过该接口拒绝,也可以选择JDK提供的四种已有策略,其特点如下:
- threadpoolexecutor.Abortpolicy:丢弃任务并抛出rejectedexecutionexception异常.这是线程池默认的拒绝策略,在任务不能再提交的时候抛出异常,及时反馈程序运行状态.如果是比较关键的业务推荐使用该策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现.
- threadpoolexecutor.DiscardPolicy:丢弃任务不抛出异常,这可能会使我们无法发现系统的异常状态.建议一些无关紧要的业务采用此策略。
- ThreadpoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务.是都要采用此种拒绝策略,还得根据实际业务是否允许丢弃老人无来认真衡量.
- threadpoolexecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务,这种情况是需要让所有的任务都执行完毕,那么就适合大量计算的任务类型去执行,多线程仅仅是增大吞吐量的手段,最终必须要让每个任务都执行完毕。