线程池相信大家都用过,定义一个线程池,然后就是往线程池提交任务,最后线程池就执行任务。
这里面有两个问题需要探讨:
1)提交任务后,线程池如何执行任务?
2)线程池线程如何重复利用核心线程执行随时提交的任务?
下面咱们针对这两个问题进行分析。
一 线程池创建
在Java 并发包下,提供了Executors工厂创建线程池,但是一般不建议调用工厂方法创建线程池,
建议自定义线程池。调用线程工厂创建线程池或自定义线程池最后都逃不过调用
ThreadPoolExecutor的宿命,看下ThreadPoolExecutor构造方法的源代码:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
corePoolSize:线程池的核心线程数。每一次任务的提交只要当前线程池已创建线程数小于核心线程数,都会创建线程执行任务,
无论你当前线程池是否有空闲线程。这些线程是全职工,即便没有任务执行,他们也会等待任务。
也即,当线程池里没有任务任务时,也会有corePoolSize个忠诚的线程在等着执行任务。
maximumPoolSize:线程池最大线程池数。无论你提交多少任务,线程池里最多能创建maximumPoolSize个工作线程。
keepAliveTime:线程的存活时间。这个存活时间控制的是大于corePoolSize之后创建的线程,如果这部分线程
等待keepAliveTime时间后,还没任务处理,哪么这部分线程就会退出线程池,可以比喻成线程池的临时工,就像饭店一样,
生意火的时候,全职的员工忙不过来,找了些临时工来帮忙,当没啥事干的时候,这部分临时工就辞退了,就像这部分线程
退出了一样。
unit:指定keepAliveTime的单位。比如,TimeUnit.SECONDS,表示单位为秒。
workQueue:阻塞队列。当核心线程处理不过来的时候,任务就放入队列里面等待执行。
threadFactory:线程工厂。用于创建线程池线程。
handler:饱和策略。当线程池达到饱和之后,提交的任务交给饱和策略处理。
二 线程池工作原理
线程池工作原理图:
1)任务提交到线程池,首先判断线程数是否小于corePoolSize,如果小于,则创建线程执行任务,
无论是否有空闲线程。
2)如果核心线程已满,判断阻塞队列是否已满,如果未满,则将任务加入队列等待执行。
3)如果阻塞队列已满,判断线程池数是否小于maximumPoolSize,如果小于,创建线程执行任务。
4)如果线程池已满,提交的任务交由饱和策略处理(handler)。
下面基于jdk1.8.0_181,从源码层面分析线程池工作原理。
1、线程池提交任务的execute()方法
源码位置:java.util.concurrent.ThreadPoolExecutor#execute
这四个步骤对应上图线程池工作原理图。
1)任务提交到线程池,首先判断线程池中当前工作线程数是否小于corePoolSize,如果小于,
则调用addWorker()创建线程执行任务。
2)如果线程池线程数不小于corePoolSize,调用阻塞队列workQueue.offer()往队列添加任务等待执行。
3)如果放入队列失败,第三步会去创建线程执行任务,在内部判断了如果线程池线程数
不小于maximumPoolSize,则创建失败,说明这个时候线程池已经达到饱和状态。
4)线程池达到饱和状态后,交由饱和策略处理,即reject()方法处理。
2、addWorker()方法
上面相当于线程池工作原理的流程概要,具体线程的创建和执行由addWorker()方法完成。
源码路径:java.util.concurrent.ThreadPoolExecutor#addWorker
addWorker方法太长,咱们分两部分分析。
这个方法接受一个boolean类型的core变量,如果大家细心的话,在execute()源码中线程数
小于corePoolSize时,调用addWorker()时传进来的是true,队列满了后再调时传的是false。
这个boolean的core取决于线程池线程数是与corePoolSize比较大小,还是与maximumPoolSize
比较大小,来决定能否创建相应的线程。
如果core是true,这个是时候是判断是否能创建核心线程,如果corePoolSize小于线程池数量,
则能创建,否则不能创建核心线程;
如果core是false,这个时候是判断是否能再创建线程,如果maximumPoolSize小于线程池数量,
则能创建,否则不能创建非核心线程。
1)这个地方是创建工作线程的地方,返回一个Worker然后从中获取Thread实例。
2)看到这个start()方法,是不是有一种久违的感觉,咱们从提交任务到线程池,一路走来,
终于看到了线程启动的方法。
3、Worker的结构
Worker继承了AQS框架,同时实现了Runnable接口,让Worker拥有了同步和线程的特性。
1)创建线程,线程工厂传进去的是this,也就是Worker,而不是我们提交的任务,
我们提交到线程池的任务被赋值为firstTask,成为了Worker的一个成员属性。
2)我们上面已经启动了线程,线程最终会执行对应启动线程的run()方法,Worker里面的run()方法
调用了runWorker方法,在runWorker里面才是真正执行我们提交的任务的地方。
4、runWorker逻辑
1)从Worker取成员变量firstTask,即为我们提交到线程池的任务。
2)while写了个死循环,如果task不为空,或者getTask()从队列里面能取到任务,while就不会退出。
3)task调用run方法,task为我们提交的任务,终于执行了我们提交的任务里面的run方法。
到这里,我们对线程池的工作原理有了一个基本的认识,但是还有一个问题就是线程池不提交任务时,
线程池的核心线程如何维持不退出一直等待获取任务执行,是因为它长得帅吗,超出核心线程数创建的哪些
线程什么时候退出了,是因为害羞了吗?
要理解这两个问题,还需要结合getTask(),看里面都干了啥。
5、getTask源码
1)如果线程池关闭了同时任务状态为Stop或队列为空时,线程数通过CAS算法减少,外层的while退出,线程退出。
2)boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
返回timed是一个boolean值。allowCoreThreadTimeOut默认为false,如果wc > corePoolSize,
也即线程池线程数大于核心线程数时,从判断条件可以知道,当队列为空,
并且TimeOut为True的时候,调用compareAndDecrementWorkerCount(c)
减少线程池数,同时,返回null,外层while循环终止,线程释放,退出的是非核心线程。
这里需要注意的是TimeOut默认是false,什么时候变成True,在下面分析。
3)如果timed为true,是非核心线程通过poll从队列获取任务,超时时间为keepAliveTime,
如果获取不到任务,TimeOut就会被设置为True,因为代码在for死循环里面,会触发上面非核心
线程退出的代码块。
如果timed为false,是核心线程从队列中通过take获取任务,如果没有任务,则阻塞,
这就保证了在线程池没有任务的时候,corePoolSize数量的核心线程不退出线程,
一直等待任务的执行,因为take拿不到值,就一直阻塞,这就是线程池没有任务执行时
还能维持核心线程不退出线程池的本质。
因为核心线程数量的线程一直等待任务的执行,这个时候如果再往线程池提交任务,
是不会创建线程的,而是任务直接加入队列,阻塞的核心线程就会通过take从队列拿到任务,
执行任务,没有任务了,核心线程又恢复阻塞。
我们通过上述了解,应该能解决下面三个问题:
1)线程池的工作原理?
2)非核心线程如何退出?
3)核心线程如何维持不退出?