002-多线程-JUC线程池-ThreadPoolExecutor运转机制详解,线程池的扩展beforeExcute,afterExcute,terminated
一、ThreadPoolExecutor理解
1.1、为什么要用线程池:
1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
1.2、参数变量
1.2.1、runState
在ThreadPoolExecutor中定义了一个volatile变量,另外定义了几个static final变量表示线程池的各个状态:
volatile int runState;
static final int RUNNING = 0;
static final int SHUTDOWN = 1;
static final int STOP = 2;
static final int TERMINATED = 3;
runState表示当前线程池的状态,它是一个volatile变量用来保证线程之间的可见性;
下面的几个static final变量表示runState可能的几个取值。
当创建线程池后,初始时,线程池处于RUNNING状态;
如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。
1.2.2、成员变量
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; //用来记录已经执行完毕的任务个数
重点解释:corePoolSize、maximumPoolSize、largestPoolSize三个变量。
corePoolSize在很多地方被翻译成核心池大小,其实这个就是线程池的大小。
举个简单的例子:
假如有一个工厂,工厂里面有10个工人,每个工人同时只能做一件任务。
因此只要当10个工人中有工人是空闲的,来了任务就分配给空闲的工人做;
当10个工人都有任务在做时,如果还来了任务,就把任务进行排队等待;
如果说新任务数目增长的速度远远大于工人做任务的速度,那么此时工厂主管可能会想补救措施,比如重新招4个临时工人进来;
然后就将任务也分配给这4个临时工人做;
如果说着14个工人做任务的速度还是不够,此时工厂主管可能就要考虑不再接收新的任务或者抛弃前面的一些任务了。
当这14个工人当中有人空闲时,而新任务增长的速度又比较缓慢,工厂主管可能就考虑辞掉4个临时工了,只保持原来的10个工人,毕竟请额外的工人是要花钱的。
这个例子中的corePoolSize就是10,而maximumPoolSize就是14(10+4)。
也就是说corePoolSize就是线程池大小,maximumPoolSize在我看来是线程池的一种补救措施,即任务量突然过大时的一种补救措施。
不过为了方便理解,在本文后面还是将corePoolSize翻译成核心池大小。
largestPoolSize只是一个用来起记录作用的变量,用来记录线程池中曾经有过的最大线程数目,跟线程池的容量没有任何关系。
1.3、构造方法及参数
1.3.1、构造方法
java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类。在ThreadPoolExecutor类中提供了四个构造方法:
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继承了AbstractExecutorService类,并提供了四个构造器,事实上,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调用的第四个构造器进行的初始化工作。
第四个构建参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
错误理解:线程池里保持corePoolSize个线程,如果不够用,就加线程入池直至maximumPoolSize大小,如果还不够就往workQueue里加,如果workQueue也不够就用RejectedExecutionHandler来做拒绝处理。
错误图示
正确流程:看-1.3.3
1.3.2、构造参数
1、corePoolSize、prestartCoreThread、prestartAllCoreThreads
核心池的大小,这个参数跟线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
初始化:默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。其中中间任务少于已创建的线程时,核心配置100个,创建了10个后,任务比较少,这时来了一个,也会继续创建新的,直至创建100个。
在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:动态的预开启所有的线程数。
prestartCoreThread():初始化一个核心线程;
prestartAllCoreThreads():初始化所有核心线程
下面是这2个方法的实现:
public boolean prestartCoreThread() { return addIfUnderCorePoolSize(null); //注意传进去的参数是null }
public int prestartAllCoreThreads() { int n = 0; while (addIfUnderCorePoolSize(null))//注意传进去的参数是null ++n; return n; }
注意上面传进去的参数是null,根据以后的分析可知如果传进去的参数为null,则最后执行线程会阻塞在getTask方法中的r = workQueue.take();即等待任务队列中有任务。
2、maximumPoolSize :线程池最大线程数,它表示在线程池中最多能创建多少个线程;
3、keepAliveTime
表示线程没有任务执行时最多保持多久时间会终止。
默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。
但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
4、unit
参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:TimeUnit.具体时间单位
5、workQueue
BlockingQueue<Runnable>类型,一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,通常可以取下面三种类型:
1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
有界队列。如ArrayBlockingQueue,当定义了maximumPoolSizes时使用有界队列可以预防资源的耗尽,但是增加了调整和控制队列的难度,队列的大小和线程池的大小是相互影响的,使用很大的队列和较小的线程池会减少CPU消耗、操作系统资源以及线程上下文开销,但却人为的降低了吞吐量。如果任务是频繁阻塞型的(I/O),系统是可以把时间片分给多个线程的。而采用较小的队列和较大的线程池,虽会造成CPU繁忙,但却会遇到调度开销,这也会降低吞吐量。
2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
无界队列。如LinkedBlockingQueue,当核心线程正在工作时,使用不用预先定义大小的无界队列将使新到来的任务处理等到中,所以如果线程数是小于corePoolSize时,将不会创建有入队操作。这种策略将很适合那些相互独立的任务,如Web服务器。如果新增任务的速度大于任务处理的速度就会造成无界队列一直增长的可能性。
3)synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
直接传递。SynchronousQueue队列的默认方式,一个存储元素的阻塞队列而是直接投递到线程中。每一个入队操作必须等到另一个线程调用移除操作,否则入队将一直阻塞。当处理一些可能有内部依赖的任务时,这种策略避免了加锁操作。直接传递一般不能限制maximumPoolSizes以避免拒绝 接收新的任务。如果新增任务的速度大于任务处理的速度就会造成增加无限多的线程的可能性。
ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
超出一定数量的任务会转移队列中,队列与池里的线程大小的关联表现在:如运行的线程数小于corePoolSize线程数,Executor会优先添加线程来执行task,而不会添加到队列中。如运行的线程已大于 corePoolSize,Executor会把新的任务放于队列中,如队列已到最大时,ThreadPoolExecutor会继续创建线程,直到超过 maximumPoolSize。最后,线程超过maximumPoolSize时,Executor将拒绝接收新的task.
6、threadFactory
线程工厂,主要用来创建线程;新线程是使用java.util.concurrent.ThreadFactory来创建的,如果没有指定其他的方式,则是使用Executors.defaultThreadFactory方法,默认创建的线程拥有相同线程组与优先级且都是非后台线程。
7、handler 饱和策略(拒绝接收任务)【使用线程池必须配置】
当Executor调用shutdown方法后或者任务缓存队列已满达到工作队列的最容量maximumPoolSize时,线程池则已经饱和了,此时则不会接收新的task。
如果还有任务到来就会采取任务拒绝策略,execute方法会调用RejectedExecutionHandler#rejectedExecution方法来执行饱和策略,表示当拒绝处理任务时的策略,通常有以下四种策略:
ThreadPoolExecutor.AbortPolicy:默认策略, 丢弃任务,Executor会抛出一个RejectedExecutionException运行异常到调用者线程来完成终止。
ThreadPoolExecutor.DiscardPolicy:不处理,也是丢弃任务,不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)如果Executor还未shutdown的话,则丢弃工作队列的最近的一个任务,然后执行当前任务。
ThreadPoolExecutor.CallerRunsPolicy:这种策略会由调用execute方法的线程自身来执行任务,由调用线程处理该任务,它提供了一个简单的反馈机制并能降低新任务的提交频率。
线程池默认会采用的是defaultHandler策略。首先看defaultHandler的定义:
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy(); // 使用默认的拒绝策略
策略
//丢弃任务并抛出RejectedExecutionException异常。 public static class AbortPolicy implements RejectedExecutionHandler { public AbortPolicy() { } // 抛出异常 public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString()); } }
7.1、AbortPolicy:默认策略, 丢弃任务,Executor会抛出一个RejectedExecutionException运行异常到调用者线程来完成终止。
@Test public void testAbortPolicy() throws Exception { // 创建线程池。线程池的"最大池大小"和"核心池大小"都为1(THREADS_SIZE),"线程池"的阻塞队列容量为1(CAPACITY)。 ThreadPoolExecutor pool = new ThreadPoolExecutor(THREADS_SIZE, THREADS_SIZE, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(CAPACITY)); pool.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); for (int i = 0; i < 10; i++) { final int a=i; pool.execute(() -> { try { System.out.println(new Date() + ":" + Thread.currentThread().getName()+"-"+a + ";守护线程:" + Thread.currentThread().isDaemon()); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } }); } Thread.sleep(3000); // 关闭线程池 pool.shutdown(); }
输出:
Tue Sep 03 16:44:44 CST 2019:pool-1-thread-1-0;守护线程:false java.util.concurrent.RejectedExecutionException: Task com.github.bjlhx15.common.base.thread.spring.controller.ThreadPoolRejectedExecutionHandler$$Lambda$1/1749186397@5ecddf8f rejected from java.util.concurrent.ThreadPoolExecutor@3f102e87[Running, pool size = 1, active threads = 1, queued tasks = 1, completed tasks = 0] at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063) ……
执行了第一个后续就拒绝了,测试过程中不确定,有时是执行0个,1个,2个。但确定会有异常抛出
7.2、DiscardPolicy:不处理,也是丢弃任务,不抛出异常。
@Test public void testDiscardPolicy() throws Exception { // 创建线程池。线程池的"最大池大小"和"核心池大小"都为1(THREADS_SIZE),"线程池"的阻塞队列容量为1(CAPACITY)。 ThreadPoolExecutor pool = new ThreadPoolExecutor(THREADS_SIZE, THREADS_SIZE, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(CAPACITY)); pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy()); for (int i = 0; i < 10; i++) { final int a=i; pool.execute(() -> { try { System.out.println(new Date() + ":" + Thread.currentThread().getName()+"-"+a + ";守护线程:" + Thread.currentThread().isDaemon()); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } }); } Thread.sleep(3000); // 关闭线程池 pool.shutdown(); }
输出
Tue Sep 03 16:46:10 CST 2019:pool-1-thread-1-0;守护线程:false Tue Sep 03 16:46:10 CST 2019:pool-1-thread-1-1;守护线程:false
执行了前两个
线程池pool的”最大池大小”和”核心池大小”都为1(THREADS_SIZE),这意味着”线程池能同时运行的任务数量最大只能是1”。
线程池pool的阻塞队列是ArrayBlockingQueue,ArrayBlockingQueue是一个有界的阻塞队列,ArrayBlockingQueue的容量为1。这也意味着线程池的阻塞队列只能有一个线程池阻塞等待。
根据”“中分析的execute()代码可知:线程池中共运行了2个任务。第1个任务直接放到Worker中,通过线程去执行;第2个任务放到阻塞队列中等待。其他的任务都被丢弃了!
7.3、DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)如果Executor还未shutdown的话,则丢弃工作队列的最近的一个任务,然后执行当前任务。
@Test public void testDiscardOldestPolicy() throws Exception { // 创建线程池。线程池的"最大池大小"和"核心池大小"都为1(THREADS_SIZE),"线程池"的阻塞队列容量为1(CAPACITY)。 ThreadPoolExecutor pool = new ThreadPoolExecutor(THREADS_SIZE, THREADS_SIZE, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(CAPACITY)); // 设置线程池的拒绝策略为"丢弃" pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy()); for (int i = 0; i < 10; i++) { final int a=i; pool.execute(() -> { try { System.out.println(new Date() + ":" + Thread.currentThread().getName()+"-"+a + ";守护线程:" + Thread.currentThread().isDaemon()); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } }); } Thread.sleep(3000); // 关闭线程池 pool.shutdown(); }
输出
Tue Sep 03 16:47:16 CST 2019:pool-1-thread-1-0;守护线程:false Tue Sep 03 16:47:17 CST 2019:pool-1-thread-1-9;守护线程:false
执行了第一个和最后一个,当有任务添加到线程池被拒绝时,线程池会丢弃阻塞队列中末尾的任务,然后将被拒绝的任务添加到末尾。
7.4、CallerRunsPolicy:这种策略会由调用execute方法的线程自身来执行任务,由调用线程处理该任务,它提供了一个简单的反馈机制并能降低新任务的提交频率。
@Test public void testCallerRunsPolicy() throws Exception { // 创建线程池。线程池的"最大池大小"和"核心池大小"都为1(THREADS_SIZE),"线程池"的阻塞队列容量为1(CAPACITY)。 ThreadPoolExecutor pool = new ThreadPoolExecutor(THREADS_SIZE, THREADS_SIZE, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(CAPACITY)); pool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); for (int i = 0; i < 10; i++) { final int a=i; pool.execute(() -> { try { System.out.println(new Date() + ":" + Thread.currentThread().getName()+"-"+a + ";守护线程:" + Thread.currentThread().isDaemon()); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } }); } Thread.sleep(3000); // 关闭线程池 pool.shutdown(); }
输出
Tue Sep 03 16:50:04 CST 2019:pool-1-thread-1-0;守护线程:false Tue Sep 03 16:50:04 CST 2019:main-2;守护线程:false Tue Sep 03 16:50:05 CST 2019:pool-1-thread-1-1;守护线程:false Tue Sep 03 16:50:05 CST 2019:main-4;守护线程:false Tue Sep 03 16:50:05 CST 2019:main-5;守护线程:false Tue Sep 03 16:50:05 CST 2019:pool-1-thread-1-3;守护线程:false Tue Sep 03 16:50:05 CST 2019:main-7;守护线程:false Tue Sep 03 16:50:05 CST 2019:pool-1-thread-1-6;守护线程:false Tue Sep 03 16:50:05 CST 2019:pool-1-thread-1-8;守护线程:false Tue Sep 03 16:50:05 CST 2019:main-9;守护线程:false
全部运行:当有任务添加到线程池被拒绝时,线程池会将被拒绝的任务添加到”线程池正在运行的线程”中取运行。
1.3.3、小结
正确流程如下:
1)如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个新的线程去执行这个任务;
2)如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
3)当workQueue放不下新入的任务时,新建线程入池,并处理请求,如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;使用RejectedExecutionHandler来做拒绝处理
4)另外,当池子的线程数大于corePoolSize的时候,多余的线程会等待keepAliveTime长的时间,如果某线程空闲时间超过keepAliveTime,此时无请求可处理就自行销毁,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。
1.4、核心方法
1.4.1、execute
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) { if (runState == RUNNING && workQueue.offer(command)) { if (runState != RUNNING || poolSize == 0) ensureQueuedTaskHandled(command); } else if (!addIfUnderMaximumPoolSize(command)) reject(command); // is shutdown or saturated } }
解析:
①首先,判断提交的任务command是否为null,若是null,则抛出空指针异常;
②接着是这句:if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command))
由于是或条件运算符,所以先计算前半部分的值,如果线程池中当前线程数不小于核心池大小,那么就会直接进入下面的if语句块了。
如果线程池中当前线程数小于核心池大小,则接着执行后半部分,也就是执行:addIfUnderCorePoolSize(command)
如果执行完addIfUnderCorePoolSize这个方法返回false,则继续执行下面的if语句块,否则整个方法就直接执行完毕了。
③如果执行完addIfUnderCorePoolSize这个方法返回false,然后接着判断:
if (runState == RUNNING && workQueue.offer(command))
如果当前线程池处于RUNNING状态,则将任务放入任务缓存队列;如果当前线程池不处于RUNNING状态或者任务放入缓存队列失败,则执行:addIfUnderMaximumPoolSize(command)
③.1如果执行addIfUnderMaximumPoolSize方法失败,则执行reject()方法进行任务拒绝处理。
③.2回到前面:if (runState == RUNNING && workQueue.offer(command))
这句的执行,如果说当前线程池处于RUNNING状态且将任务放入任务缓存队列成功,则继续进行判断:if (runState != RUNNING || poolSize == 0)
这句判断是为了防止在将此任务添加进任务缓存队列的同时其他线程突然调用shutdown或者shutdownNow方法关闭了线程池的一种应急措施。如果是这样就执行:
ensureQueuedTaskHandled(command)
进行应急处理,从名字可以看出是保证添加到任务缓存队列中的任务得到处理。
注:2个关键方法的实现addIfUnderCorePoolSize和addIfUnderMaximumPoolSize
addIfUnderCorePoolSize实现:
private boolean addIfUnderCorePoolSize(Runnable firstTask) { Thread t = null; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { if (poolSize < corePoolSize && runState == RUNNING) t = addThread(firstTask); //创建线程去执行firstTask任务 } finally { mainLock.unlock(); } if (t == null) return false; t.start(); return true; }
这个是addIfUnderCorePoolSize方法的具体实现,从名字可以看出它的意图就是当低于核心池大小时执行的方法。下面看其具体实现,首先获取到锁,因为这地方涉及到线程池状态的变化,先通过if语句判断当前线程池中的线程数目是否小于核心池大小,疑问:前面在execute()方法中不是已经判断过了吗,只有线程池当前线程数目小于核心池大小才会执行addIfUnderCorePoolSize方法的,为何这地方还要继续判断?原因很简单,前面的判断过程中并没有加锁,因此可能在execute方法判断的时候poolSize小于corePoolSize,而判断完之后,在其他线程中又向线程池提交了任务,就可能导致poolSize不小于corePoolSize了,所以需要在这个地方继续判断。然后接着判断线程池的状态是否为RUNNING,原因也很简单,因为有可能在其他线程中调用了shutdown或者shutdownNow方法。然后就是执行t = addThread(firstTask);
这个方法也非常关键,传进去的参数为提交的任务,返回值为Thread类型。然后接着在下面判断t是否为空,为空则表明创建线程失败(即poolSize>=corePoolSize或者runState不等于RUNNING),否则调用t.start()方法启动线程。
addThread方法的实现::
private Thread addThread(Runnable firstTask) { Worker w = new Worker(firstTask); Thread t = threadFactory.newThread(w); //创建一个线程,执行任务 if (t != null) { w.thread = t; //将创建的线程的引用赋值为w的成员变量 workers.add(w); int nt = ++poolSize; //当前线程数加1 if (nt > largestPoolSize) largestPoolSize = nt; } return t; }
在addThread方法中,首先用提交的任务创建了一个Worker对象,然后调用线程工厂threadFactory创建了一个新的线程t,然后将线程t的引用赋值给了Worker对象的成员变量thread,接着通过workers.add(w)将Worker对象添加到工作集当中。
Worker类的实现:
private final class Worker implements Runnable { private final ReentrantLock runLock = new ReentrantLock(); private Runnable firstTask; volatile long completedTasks; Thread thread; Worker(Runnable firstTask) { this.firstTask = firstTask; } boolean isActive() { return runLock.isLocked(); } void interruptIfIdle() { final ReentrantLock runLock = this.runLock; if (runLock.tryLock()) { try { if (thread != Thread.currentThread()) thread.interrupt(); } finally { runLock.unlock(); } } } void interruptNow() { thread.interrupt(); } private void runTask(Runnable task) { final ReentrantLock runLock = this.runLock; runLock.lock(); try { if (runState < STOP && Thread.interrupted() && runState >= STOP) boolean ran = false; beforeExecute(thread, task); //beforeExecute方法是ThreadPoolExecutor类的一个方法,没有具体实现,用户可以根据 //自己需要重载这个方法和后面的afterExecute方法来进行一些统计信息,比如某个任务的执行时间等 try { task.run(); ran = true; afterExecute(task, null); ++completedTasks; } catch (RuntimeException ex) { if (!ran) afterExecute(task, ex); throw ex; } } finally { runLock.unlock(); } } public void run() { try { Runnable task = firstTask; firstTask = null; while (task != null || (task = getTask()) != null) { runTask(task); task = null; } } finally { workerDone(this); //当任务队列中没有任务时,进行清理工作 } } }
它实际上实现了Runnable接口,因此上面的Thread t = threadFactory.newThread(w);效果跟下面这句的效果基本一样:Thread t = new Thread(w);
相当于传进去了一个Runnable任务,在线程t中执行这个Runnable。
既然Worker实现了Runnable接口,那么自然最核心的方法便是run()方法:
public void run() { try { Runnable task = firstTask; firstTask = null; while (task != null || (task = getTask()) != null) { runTask(task); task = null; } } finally { workerDone(this); } }
从run方法的实现可以看出,它首先执行的是通过构造器传进来的任务firstTask,在调用runTask()执行完firstTask之后,在while循环里面不断通过getTask()去取新的任务来执行,那么去哪里取呢?自然是从任务缓存队列里面去取,getTask是ThreadPoolExecutor类中的方法,并不是Worker类中的方法,下面是getTask方法的实现:
Runnable getTask() { for (;;) { try { int state = runState; if (state > SHUTDOWN) return null; Runnable r; if (state == SHUTDOWN) // Help drain queue r = workQueue.poll(); else if (poolSize > corePoolSize || allowCoreThreadTimeOut) //如果线程数大于核心池大小或者允许为核心池线程设置空闲时间, //则通过poll取任务,若等待一定的时间取不到任务,则返回null r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS); else r = workQueue.take(); if (r != null) return r; if (workerCanExit()) { //如果没取到任务,即r为null,则判断当前的worker是否可以退出 if (runState >= SHUTDOWN) // Wake up others interruptIdleWorkers(); //中断处于空闲状态的worker return null; } // Else retry } catch (InterruptedException ie) { // On interruption, re-check runState } } }
在getTask中,先判断当前线程池状态,如果runState大于SHUTDOWN(即为STOP或者TERMINATED),则直接返回null。
如果runState为SHUTDOWN或者RUNNING,则从任务缓存队列取任务。
如果当前线程池的线程数大于核心池大小corePoolSize或者允许为核心池中的线程设置空闲存活时间,则调用poll(time,timeUnit)来取任务,这个方法会等待一定的时间,如果取不到任务就返回null。
然后判断取到的任务r是否为null,为null则通过调用workerCanExit()方法来判断当前worker是否可以退出,我们看一下workerCanExit()的实现:
private boolean workerCanExit() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); boolean canExit; //如果runState大于等于STOP,或者任务缓存队列为空了 //或者 允许为核心池线程设置空闲存活时间并且线程池中的线程数目大于1 try { canExit = runState >= STOP || workQueue.isEmpty() || (allowCoreThreadTimeOut && poolSize > Math.max(1, corePoolSize)); } finally { mainLock.unlock(); } return canExit; }
也就是说如果线程池处于STOP状态、或者任务队列已为空或者允许为核心池线程设置空闲存活时间并且线程数大于1时,允许worker退出。如果允许worker退出,则调用interruptIdleWorkers()中断处于空闲状态的worker,我们看一下interruptIdleWorkers()的实现:
void interruptIdleWorkers() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { for (Worker w : workers) //实际上调用的是worker的interruptIfIdle()方法 w.interruptIfIdle(); } finally { mainLock.unlock(); } }
从实现可以看出,它实际上调用的是worker的interruptIfIdle()方法,在worker的interruptIfIdle()方法中:
void interruptIfIdle() { final ReentrantLock runLock = this.runLock; if (runLock.tryLock()) { //注意这里,是调用tryLock()来获取锁的,因为如果当前worker正在执行任务,锁已经被获取了,是无法获取到锁的 //如果成功获取了锁,说明当前worker处于空闲状态 try { if (thread != Thread.currentThread()) thread.interrupt(); } finally { runLock.unlock(); } } }
这里有一个非常巧妙的设计方式,假如我们来设计线程池,可能会有一个任务分派线程,当发现有线程空闲时,就从任务缓存队列中取一个任务交给空闲线程执行。但是在这里,并没有采用这样的方式,因为这样会要额外地对任务分派线程进行管理,无形地会增加难度和复杂度,这里直接让执行完任务的线程去任务缓存队列里面取任务来执行。
我们再看addIfUnderMaximumPoolSize方法的实现,这个方法的实现思想和addIfUnderCorePoolSize方法的实现思想非常相似,唯一的区别在于addIfUnderMaximumPoolSize方法是在线程池中的线程数达到了核心池大小并且往任务队列中添加任务失败的情况下执行的:
private boolean addIfUnderMaximumPoolSize(Runnable firstTask) { Thread t = null; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { if (poolSize < maximumPoolSize && runState == RUNNING) t = addThread(firstTask); } finally { mainLock.unlock(); } if (t == null) return false; t.start(); return true; }
其实它和addIfUnderCorePoolSize方法的实现基本一模一样,只是if语句判断条件中的poolSize < maximumPoolSize不同而已。
1.4.2、线程池的关闭
ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:
shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
1.4.3、线程池容量的动态调整
ThreadPoolExecutor提供了动态调整线程池容量大小的方法:setCorePoolSize()和setMaximumPoolSize(),
setCorePoolSize:设置核心池大小
setMaximumPoolSize:设置线程池最大能创建的线程数目大小
当上述参数从小变大时,ThreadPoolExecutor进行线程赋值,还可能立即创建新的线程来执行任务。
1.4.4、提交线程执行
<T> Future<T> submit(Callable<T> task) 提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future。 Future<?> submit(Runnable task) 提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
Future 是一个接口,它表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get 方法来获取结果,如有必要,计算完成前可以阻塞此方法。取消则由 cancel 方法来执行。还提供了其他方法,以确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明 Future<?> 形式类型、并返回 null 作为底层任务的结果。那如何取得一个提交线程的运行结果呢?
我们知道Runnable中不提供线程结果的返回方法,所以我们要寻求新的工具类或者接口,它就是Callable。
Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常。
Callable的应用--异步求和案例
package com.lhx.common.thread; import java.util.concurrent.*; public class ThreadCallableTest { public static void main(String args[]) throws ExecutionException, InterruptedException { // 可以执行Runnable对象或者Callable对象代表的线程 ExecutorService pool = Executors.newFixedThreadPool(2); Future<Integer> f1 = pool.submit(new MyCallable(100)); Future<Integer> f2 = pool.submit(new MyCallable(200)); // V get() Integer i1 = f1.get(); Integer i2 = f2.get(); System.out.println(i1); System.out.println(i2); // 结束 pool.shutdown(); } } class MyCallable implements Callable<Integer> { private int number; public MyCallable(int number) { this.number = number; } @Override public Integer call() throws Exception { int sum = 0; for (int x = 1; x <= number; x++) { sum += x; } return sum; } }
1.5、内部结构图
从中可以发现ThreadPoolExecutor就是依靠BlockingQueue的阻塞机制来维持线程池,当池子里的线程无事可干的时候就通过workQueue.take()阻塞住。
1.6、原理探究
ThreadPoolExecutor继承了抽象类AbstractExecutorService,AbstractExecutorService的实现:
public abstract class AbstractExecutorService implements ExecutorService { protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) { }; protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) { }; public Future<?> submit(Runnable task) {}; public <T> Future<T> submit(Runnable task, T result) { }; public <T> Future<T> submit(Callable<T> task) { }; private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks, boolean timed, long nanos) throws InterruptedException, ExecutionException, TimeoutException { }; public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException { }; public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { }; public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException { }; public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException { }; }
AbstractExecutorService是一个抽象类,它实现了ExecutorService接口。ExecutorService接口的实现:
public interface ExecutorService extends Executor { void shutdown(); boolean isShutdown(); boolean isTerminated(); boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException; <T> Future<T> submit(Callable<T> task); <T> Future<T> submit(Runnable task, T result); Future<?> submit(Runnable task); <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException; <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException; <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException; <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; }
而ExecutorService又是继承了Executor接口,我们看一下Executor接口的实现:
public interface Executor { void execute(Runnable command); }
至此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()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。
submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果。
shutdown()和shutdownNow()是用来关闭线程池的。
还有很多其他的方法:
比如:getQueue() 、getPoolSize() 、getActiveCount()、getCompletedTaskCount()等获取与线程池相关属性的方法,可以自行查阅API。
二、线程池的扩展
2.1、线程池的扩展
JDK允许开发人员自主扩展线程池,通过提供的beforeExcute,afterExcute,terminated三个接口可以向处理aop一样方便的管理线程池,可自行实现状态跟踪,调试信息等用以监控线程池。
在执行任务的线程中将调用beforeExecute和afterExecute,这些方法中还可以添加日志,计时,监视或统计收集的功能。无论任务是从run中正常返回,还是抛出一个异常而返回,afterExecute都会被调用。如果任务在完成后带有一个Error,那么就不会调用afterExecute。如果beforeExecute抛出一个RuntimeException,那么任务将不被执行,并且afterExecute也不会被调用。
在线程池完成关闭时调用terminated,也就是在所有任务都已经完成并且所有工作者线程也已经关闭后,terminated可以用来释放Executor在其生命周期里分配的各种资源,此外还可以执行发送通知、记录日志或者手机finalize统计等操作。
示例:线程池添加统计信息为例(添加日志和及时等功能):
继承线程池
import java.util.concurrent.BlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Logger; public class TimingThreadPool extends ThreadPoolExecutor { private final ThreadLocal<Long> startTime = new ThreadLocal<Long>(); private final Logger log = Logger.getAnonymousLogger(); private final AtomicLong numTasks = new AtomicLong(); private final AtomicLong totalTime = new AtomicLong(); public TimingThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); } protected void beforeExecute(Thread t, Runnable r) { super.beforeExecute(t, r); log.info(String.format("Thread %s: start %s", t, r)); startTime.set(System.nanoTime()); } protected void afterExecute(Runnable r, Throwable t) { try { long endTime = System.nanoTime(); long taskTime = endTime - startTime.get(); numTasks.incrementAndGet(); totalTime.addAndGet(taskTime); log.info(String.format("Thread %s: end %s, time=%dns", t, r, taskTime)); } finally { super.afterExecute(r, t); } } protected void terminated() { try { log.info(String.format("Terminated: avg time=%dns", totalTime.get() / numTasks.get())); } finally { super.terminated(); } } }
示例:
public class CheckTimingThreadPool { public static void main(String[] args) { ThreadPoolExecutor exec = new TimingThreadPool(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); exec.execute(new DoSomething(5)); exec.execute(new DoSomething(4)); exec.execute(new DoSomething(3)); exec.execute(new DoSomething(2)); exec.execute(new DoSomething(1)); exec.shutdown(); } } class DoSomething implements Runnable { private int sleepTime; public DoSomething(int sleepTime) { this.sleepTime = sleepTime; } @Override public void run() { System.out.println(Thread.currentThread().getName() + " is running."); try { TimeUnit.SECONDS.sleep(sleepTime); } catch (InterruptedException e) { e.printStackTrace(); } } }
输出
六月 19, 2019 6:31:57 下午 com.github.bjlhx15.datastructure.algorithm.thread.TimingThreadPool beforeExecute 信息: Thread Thread[pool-1-thread-1,5,main]: start com.github.bjlhx15.datastructure.algorithm.thread.DoSomething@7664bc93 pool-1-thread-1 is running. 六月 19, 2019 6:31:57 下午 com.github.bjlhx15.datastructure.algorithm.thread.TimingThreadPool beforeExecute 信息: Thread Thread[pool-1-thread-2,5,main]: start com.github.bjlhx15.datastructure.algorithm.thread.DoSomething@7493fb6c pool-1-thread-2 is running. 六月 19, 2019 6:31:57 下午 com.github.bjlhx15.datastructure.algorithm.thread.TimingThreadPool beforeExecute 信息: Thread Thread[pool-1-thread-5,5,main]: start com.github.bjlhx15.datastructure.algorithm.thread.DoSomething@39dd2359 pool-1-thread-5 is running. 六月 19, 2019 6:31:57 下午 com.github.bjlhx15.datastructure.algorithm.thread.TimingThreadPool beforeExecute pool-1-thread-4 is running. 信息: Thread Thread[pool-1-thread-4,5,main]: start com.github.bjlhx15.datastructure.algorithm.thread.DoSomething@6692d9f7 pool-1-thread-3 is running. 六月 19, 2019 6:31:57 下午 com.github.bjlhx15.datastructure.algorithm.thread.TimingThreadPool beforeExecute 信息: Thread Thread[pool-1-thread-3,5,main]: start com.github.bjlhx15.datastructure.algorithm.thread.DoSomething@6b882b06 六月 19, 2019 6:31:58 下午 com.github.bjlhx15.datastructure.algorithm.thread.TimingThreadPool afterExecute 信息: Thread null: end com.github.bjlhx15.datastructure.algorithm.thread.DoSomething@39dd2359, time=1000107698ns 六月 19, 2019 6:31:59 下午 com.github.bjlhx15.datastructure.algorithm.thread.TimingThreadPool afterExecute 信息: Thread null: end com.github.bjlhx15.datastructure.algorithm.thread.DoSomething@6692d9f7, time=2001694826ns 六月 19, 2019 6:32:00 下午 com.github.bjlhx15.datastructure.algorithm.thread.TimingThreadPool afterExecute 信息: Thread null: end com.github.bjlhx15.datastructure.algorithm.thread.DoSomething@6b882b06, time=3001835956ns 六月 19, 2019 6:32:01 下午 com.github.bjlhx15.datastructure.algorithm.thread.TimingThreadPool afterExecute 信息: Thread null: end com.github.bjlhx15.datastructure.algorithm.thread.DoSomething@7493fb6c, time=4000267011ns 六月 19, 2019 6:32:02 下午 com.github.bjlhx15.datastructure.algorithm.thread.TimingThreadPool afterExecute 信息: Thread null: end com.github.bjlhx15.datastructure.algorithm.thread.DoSomething@7664bc93, time=5001373898ns 六月 19, 2019 6:32:02 下午 com.github.bjlhx15.datastructure.algorithm.thread.TimingThreadPool terminated 信息: Terminated: avg time=3001055877ns
线程池的正确使用
以下阿里编码规范里面说的一段话:
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor: 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool: 主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
2.2、线程池的优化
主要是针对数量进行,一般来说只要使用的不是最大最小线程池数量都可以,如,批量运算CPU密集型,线程数==CPU数;如有大量阻塞,可以设置线程数是CPU核心数的偶数倍
推荐公式:
NCPU = CPU的数量
UCPU = 期望对CPU的使用率 0 ≤ UCPU ≤ 1
W/C = 等待时间与计算时间的比率
如果希望处理器达到理想的使用率,那么线程池的最优大小为:
线程池大小=NCPU *UCPU(1+W/C)
在Java中使用
int ncpus = Runtime.getRuntime().availableProcessors();
2.3、手动创建线程池有几个注意点
1.任务独立。如何任务依赖于其他任务,那么可能产生死锁。例如某个任务等待另一个任务的返回值或执行结果,那么除非线程池足够大,否则将发生线程饥饿死锁。
2.合理配置阻塞时间过长的任务。如果任务阻塞时间过长,那么即使不出现死锁,线程池的性能也会变得很糟糕。在Java并发包里可阻塞方法都同时定义了限时方式和不限时方式。例如
Thread.join,BlockingQueue.put,CountDownLatch.await等,如果任务超时,则标识任务失败,然后中止任务或者将任务放回队列以便随后执行,这样,无论任务的最终结果是否成功,这种办法都能够保证任务总能继续执行下去。
3.设置合理的线程池大小。只需要避免过大或者过小的情况即可,上文的公式线程池大小=NCPU *UCPU(1+W/C)。
4.选择合适的阻塞队列。newFixedThreadPool和newSingleThreadExecutor都使用了无界的阻塞队列,无界阻塞队列会有消耗很大的内存,如果使用了有界阻塞队列,它会规避内存占用过大的问题,但是当任务填满有界阻塞队列,新的任务该怎么办?在使用有界队列是,需要选择合适的拒绝策略,队列的大小和线程池的大小必须一起调节。对于非常大的或者无界的线程池,可以使用SynchronousQueue来避免任务排队,以直接将任务从生产者提交到工作者线程。
下面是Thrift框架处理socket任务所使用的一个线程池,可以看一下FaceBook的工程师是如何自定义线程池的。
private static ExecutorService createDefaultExecutorService(Args args) { SynchronousQueue executorQueue = new SynchronousQueue(); return new ThreadPoolExecutor(args.minWorkerThreads, args.maxWorkerThreads, 60L, TimeUnit.SECONDS,executorQueue); }