来先吃我几问。
此时线程数小于核心线程数,并且线程都处于空闲状态,现提交一个任务,是新起一个线程还是给之前创建的线程?
李老是这样说的:If fewer than corePoolSize threads are running, try to start a new thread with the given command as its first task.
我觉得把 threads are running
去了,更合理一些,此时线程池会新起一个线程来执行这个新任务,不管老线程是否空闲。
你是如何理解核心线程的 ?
从上一个问题可以看出,线程池虽说默认是懒创建线程,但是它实际是想要快速拥有核心线程数的线程。核心线程指的是线程池承载日常任务的中坚力量,也就是说本质上线程池是需要这么些数量的线程来处理任务的,所以在懒中又急着创建它。
而最大线程数其实是为了应付突发状况。
举个装修的例子,正常情况下施工队只要 5 个人去干活,这 5 人其实就是核心线程,但是由于工头接的活太多了,导致 5 个人在约定工期内干不完,所以工头又去找了 2 个人来一起干,所以 5 是核心线程数,7 是最大线程数。
平时就是 5 个人干活,特别忙的时候就找 7 个,等闲下来就会把多余的 2 个辞了。
看到这里你可能会觉得核心线程在线程池里面会有特殊标记?
并没有,不论是核心还是非核心线程,在线程池里面都是一视同仁,当淘汰的时候不会管是哪些线程,反正留下核心线程数个线程即可,下文会作详解。
你是怎么理解 KeepAliveTime 的?
这就是上面提到的,线程池其实想要的只是核心线程数个线程,但是又预留了一些数量来预防突发状况,当突发状况过去之后,线程池希望只维持核心线程数的线程,所以就弄了个 KeepAliveTime,当线程数大于核心数之后,如果线程空闲了一段时间(KeepAliveTime),就回收线程,直到数量与核心数持平。
那 workQueue 有什么用?
缓存任务供线程获取,这里要注意限制工作队列的大小。队列长了,堆积的任务就多,堆积的任务多,后面任务等待的时长就长。
想想你点击一个按钮是一直转圈等半天没反应舒服,还是直接报错舒服,所以有时心是好的,想尽量完成提交的任务,但是用户体验不如直接拒绝。更有可能由于允许囤积的任务过多,导致资源耗尽而系统崩溃。
所以工作队列起到一个缓冲作用,具体队列长度需要结合线程数,任务的执行时长,能承受的等待时间等。
你是如何理解拒绝策略的?
线程数总有拉满的一天,工作队列也是一样,如果两者都满了,此时的提交任务就需要拒绝,默认实现是 AbortPolicy 直接抛出异常。
剩下的拒绝策略有直接丢弃任务一声不吭的、让提交任务的线程自己运行的、淘汰老的未执行的任务而空出位置的,具体用哪个策略,根据场景选择。当然也可以自定义拒绝策略,实现 RejectedExecutionHandler
这个接口即可。
所以线程池尽可能只维护核心数量的线程,提供任务队列暂存任务,并提供拒绝策略来应对过载的任务。
这里还有个细节,如果线程数已经达到核心线程数,那么新增加的任务只会往任务队列里面塞,不会直接给予某个线程,如果任务队列也满了,新增最大线程数的线程时,任务是可以直接给予新建的线程执行的,而不是入队。
感觉已经会了?那再来看几道面试题:
你说你看过源码,那你肯定知道线程池里的 ctl 是干嘛的咯?
其实并发包中有很多实现都是一个字段存多个值的,比如读写锁的高 16 位存放读锁,低 16 位存放写锁,这种一个字段存放多个值可以更容易的维护多个值之间的一致性,也算是极简主义。
你知道线程池有几种状态吗?
注解说的很明白,我再翻译一下:
- RUNNING:能接受新任务,并处理阻塞队列中的任务
- SHUTDOWN:不接受新任务,但是可以处理阻塞队列中的任务
- STOP:不接受新任务,并且不处理阻塞队列中的任务,并且还打断正在运行任务的线程,就是直接撂担子不干了!
- TIDYING:所有任务都终止,并且工作线程也为0,处于关闭之前的状态
- TERMINATED:已关闭。
你知道线程池的状态是如何变迁的吗?
为什么要把任务先放在任务队列里面,而不是把线程先拉满到最大线程数?
我说下我的个人理解。
其实经过上面的分析可以得知,线程池本意只是让核心数量的线程工作着,不论是 core 的取名,还是 keepalive 的设定,所以你可以直接把 core 的数量设为你想要线程池工作的线程数,而任务队列起到一个缓冲的作用。最大线程数这个参数更像是无奈之举,在最坏的情况下做最后的努力,去新建线程去帮助消化任务。
所以我个人觉得没有为什么,就是这样设计的,并且这样的设定挺合理。
当然如果你想要扯一扯 CPU 密集和 I/O 密集,那可以扯一扯。
原生版线程池的实现可以认为是偏向 CPU 密集的,也就是当任务过多的时候不是先去创建更多的线程,而是先缓存任务,让核心线程去消化,从上面的分析我们可以知道,当处理 CPU 密集型任务的时,线程太多反而会由于线程频繁切换的开销而得不偿失,所以优先堆积任务而不是创建新的线程。
而像 Tomcat 这种业务场景,大部分情况下是需要大量 I/O 处理的情况就做了一些定制,修改了原生线程池的实现,使得在队列没满的时候,可以创建线程至最大线程数。
如何修改原生线程池,使得可以先拉满线程数再入任务队列排队?
如果了解线程池的原理,很轻松的就知道关键点在哪,就是队列的 offer 方法。
execute 方法想必大家都不陌生,就是给线程池提交任务的方法。在这个方法中可以看到只要在 offer 方法内部判断此时线程数还小于最大线程数的时候返回 false,即可走下面 else if
中 addWorker
(新增线程)的逻辑,如果数量已经达到最大线程数,直接入队即可。
详细的我们可以看看 Tomcat 中是如何定制线程的。
Tomcat 中的定制化线程池实现
public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {}
可以看到先继承了 JUC 的线程池,然后我们重点关注一下 execute 这个方法