原作者:海子
本文归作者海子和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
1.为什么要使用线程池
当我们使用线程的时候, 就去创建一个线程, 虽然使用起来很简单, 但是如果并发的线程数量很多, 并且每个线程都是执行完一段时间很短的任务就结束了, 这样频繁的去创建线程就会大大的浪费系统的性能, 因为频繁的去创建线程和销毁线程是需要时间的。
2. 线程池的作用
线程池的作用就是执行完一个线程之后并不销毁, 而是继续去执行其他的任务, 从而达到提高系统性能的目的。
一.Java中的ThreadPoolExecutor类
java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,因此如果要透彻地了解Java中的线程池public class ThreadPoolExecutor extends AbstractExecutorService { ..... public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue); public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory); public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler); public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler); ... }
ThreadPoolExecutor提供了四种构造方法, 以下是几个参数的说明
- corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
- maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
- keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
- unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
- workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
- threadFactory:线程工厂,主要用来创建线程
- handler:表示当拒绝处理任务时的策略
ThreadPoolExecutor、AbstractExecutorService、ExecutorService和Executor 的关系Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,从字面意思可以理解,就是用来执行传进去的任务的;
然后ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等;
抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法;
然后ThreadPoolExecutor继承了类AbstractExecutorService。
所以在ThreadPoolExecutor类中有几个非常重要的方法:
execute() submit() shutdown() shutdownNow()
下面对以上几个方法进行详细解释
execute(Runable runable ) 由顶层接口Executor提供, ThreadPoolExecutor对其进行了具体的实现, 方法的参数为传入Runable对象, 通过这个方法可以向线程池提交一个任务, 交给线程池去执行。submit(Runable task) 由 ExecuteService提供, 由AbstractExecuteService进行了重写,实际底层同样是调用了execute()方法, 但是执行submit会返回执行结果
shutdown() 当线程池调用此方法时, 线程池就会马上处于shutdown状态,不能再往线程池中添加任何任务,否则会抛出RejectExcutionException异常 当线程池中的任务执行完了则会退出
shutdownNow()当线程池调用了此方法时, 会马上处于Stop状态, 并试图停止还在队列中的所有任务, 底层是调用Thred.interrupt方法去实现
shutdown 和 shutdownNow 适用场景
shutdown 所有的任务都是一个单独的task, 相互之间没有关系, 其中一个线程发生异常并不会影响其他线程, 适合用shutdown
shutdownNow 线程池中所有的任务都是做同一件事, 其中一个线程发生异常会直接影响整个结果,失去继续执行下去的意义
二、线程池实现原理
1.线程池的状态
ThreadpoolExecutor类中定义一个 volatile变量 runsState变量用来标记当前线程池的状态
volatile int runState; static final int RUNNING = 0; // 正在运行 static final int SHUTDOWN = 1; // 不在接受新的任务, 继续执行剩下的任务, 执行完成之后退出 static final int STOP = 2; // 同样不接受新的任务,并试图停止正在运行的任务 static final int TERMINATED = 3; // 已经终止2.ThreadpoolExecutor 主要变量介绍private final BlockingQueue<Runnable> workQueue; //任务缓存队列,用来存放等待执行的任务
private final ReentrantLock mainLock = new ReentrantLock(); //线程池的主要状态锁,对线程池状态(比如线程池大小 //、runState等)的改变都要使用这个锁private final HashSet<Worker> workers = new HashSet<Worker>(); //用来存放工作集
private volatile long keepAliveTime; //线程存活时间
private volatile boolean allowCoreThreadTimeOut; //是否允许为核心线程设置存活时间
private volatile int corePoolSize; //核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
private volatile int maximumPoolSize; //线程池最大能容忍的线程数
private volatile int poolSize; //线程池中当前的线程数 private volatile RejectedExecutionHandler handler; //任务拒绝策略 private volatile ThreadFactory threadFactory; //线程工厂,用来创建线程 private int largestPoolSize; //用来记录线程池中曾经出现过的最大线程数 private long completedTaskCount; //用来记录已经执行完毕的任务个数3. corePoolSize , maximumPoolSize , largestPoolSize 之间的解释
corePoolSize 为核心池的大小,
maximumPoolSize 线程池最大可容纳线程的大小
largestPoolSize 用来记录线程中曾经有过最大的线程池数目
用一个简单的例子简要说明三个变量之间关系假设一个工厂只有10个工人, 每个工人同时只能做一件事, 当工厂所下的订单增长速度远远大于工人的产出速度, 这时候老板可能就会多招收5个临时工,
用来干活, 这个时候工厂就会有15个人,当剩下的订单10个人能够完全应对的时候, 就会辞掉5个临时工, 10个人则为corePoolSize,
(10+5)则为maxmunPoolSize, 同时lagestPoolSize会记录工厂最的高人数为15个人
maxmunSize也可看做是一种临时补救措施
4.任务从提交到最后执行完毕需要经历的过程在TheadPoolExecutor最核心的方法就是execute, 虽然submit也可以提交任务, 但是submit最终调用的还是execute方法5.ThreadPoolExecutor execute方法详细解释public void execute(Runnable command) {// 1.如果传入的线程为空, 直接报空指针 if (command == null) throw new NullPointerException();
// 2.如果当前线程数量大于核心线程池容量的大小,并且线程池处于运行状态,则将线程放入执行队列中 if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {//addIfUnderCorePoolSize方法只是对 poolSize >= corePoolSize 表达式二次多线程判断
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command); // 此方法用于保证当其他线程调用shutdown方法时,仍然保证任务可以继续执行
}// 如果放入队列失败, 一般情况是队列已经满了, 会调用addIfUnderMaximumPoolSize方法尝试重新创建新的线程去执行,
// 如果当前线程没有超过设定最大的线程数,则执行成功, 但是当当前线程已经超过设定最大的线程数,则会驳回
else if (!addIfUnderMaximumPoolSize(command)) reject(command); // is shutdown or saturated } }6. 任务提交给线程池的处理策略a.当当前线程数量小于核心线程池容量的时候, 提交一个任务,则会创建一个线程进行处理b.当当前线程数量大于核心线程池容量的时候, 提交一个任务,则会添加到队列当中,等待其他线程处理完毕之后取出后执行,若队列添加失败,一般是队列已满,则会重新创建线程去处理
c.如果当前线程超过了最大线程池数量,则会采取驳回策略d.如果当前线程数量超过了核心线程池数量的情况下,某线程空闲时间超过了keepAliveTime时间,线程会终止7.线程池线程初始化一般情况创建线程池之后不会有线程,需要提交任务之后才会创建线程,如果需要创建线程池之后立即创建线程,可以调用以下两个方法办到
prestartCoreThread() 初始化一个核心线程
prestartAllCoreThread()初始化所有核心线程
8.线程池容量动态调整ThreadPoolExecutor 提供了setCoreThreadSize()和 setMaxiMumPoolThread()