前言:
在面试过程中,我们可能会被面试官经常问到有关线程池和线程池参数的相关问题,如果对于这些问题,你的心中没有明确的答案,那么在看完本篇博客后,相信你将会有所收获!
1.1 为什么要用线程池?
- 降低资源消耗:提高线程利用率,降低创建和销毁线程的消耗
- 提高响应速度:任务来了,直接有线程可用可执行,而不是先创建线程再执行
- 提高线程的可管理性:线程是稀缺资源,使用线程池可以统一分配调优监控
1.2 线程池参数的解释
1. corePoolSize
定义:corePoolSize 代表的是核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,而是一种常驻线程
- 如果核心线程数等于0,则任务执行完成后,没有任务请求进入时,就会销毁线程池中的线程
- 如果核心线程数大于0,即使本地任务执行完毕,核心线程也不会被销毁
注意:核心线程数设置太大会浪费系统资源,设置过小导致线程频繁创建
2. maxmumPoolSize
定义:maxmumPoolSize代表的是最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数
注意:比如当前任务较多,将核心线程都用完了,还无法满足需求时,此时就会创建新的线程,但是线程池内的线程总数不会超过最大线程数
3.keepAliveTime
定义:keepAliveTime 表示超出核心线程之外的线程空闲存活时间;
如果超出核心线程数的部分线程,空闲时间达到keepAliveTime值时 (可以通过使用setKeepAliveTime来设置空闲时间),则线程会被销毁掉 (直到剩下核心线程数个线程为止)
注意:
- 默认情况下,线程池的最大线程数大于核心线程数时,keepAliveTime才会起到作用
- 如果allowCoreThreadTimeOut设置为true,即使线程池的最大线程数等于核心线程数,keepAliveTime也会起作用 (回收超时的核心线程)
4. TimeUnit
TimeUnit是keepAliveTime的时间单位
5. workQueue
定义:
workQueue表示缓存队列,用来存放待执行的任务;当请求任务数大于核心线程数时,线程进入阻塞队列 (BlockingQueue)
使用:
- 假设现在核心线程都已被使用,还有任务进来,则全部放入阻塞队列,直到整个队列被放满
- 如果任务还在持续进入,则会开始创建新的线程 (前提是阻塞队列虽满,但线程池内线程数未达到最大线程数)
6.threadFactory
定义:threadFactory实际上是一个线程工厂,用来生产同一个组内的线程来执行任务
作用:
-
主要用于设置生成的线程名称前缀,是否为守护线程以及优先级等
-
设置有意义的名称前缀有利于在进行虚拟机分析时,知道线程是由哪个线程工厂创建的
使用:
- 我们可以选择使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程
- 当然我们也可以选择自定义线程工厂,一般我们会根据业务来指定不同的线程工厂
7.handler
定义:handler表示执行拒绝策略对象
作用:当任务缓存达到上限时 (即超过workQueue参数能存储的任务数) ,然后就执行拒绝策略,可以看做简单的限流保护
使用:分两种情况
- 第一种是当我们调用shutdown等的方法关闭线程池后,这时即使线程池内还有没执行完的任务正在执行,但是由于线程池已经关闭,我们在继续向线程池提交任务就会遭到拒绝
- 另一种情况就是当达到最大线程数时,线程池已经没有能力继续处理新提交的任务时,这时也就执行拒绝策略
1.3 四种拒绝策略
有四种拒绝策略:这些拒绝策略都是ThreadPoolExecutor (线程池执行器) 的方法
-
AbortPolicy:丢弃任务,并抛出 RejectExecutionException (拒绝执行异常)
-
CallerRunsPolicy:该任务被线程池拒绝,由调用execute方法的线程 (即提交任务的线程) 的处理该任务
-
DiscardOldestPolicy:抛弃队列最前面的任务,然后重新提交被拒绝的任务
-
DiscardPolicy:丢弃任务,不过不会抛出异常
总结:当线程池的任务缓存队列 (workQueue) 已满,并且线程池中的线程数目达到最大线程数 (maximumPoolSize) ,如果还有任务到来就会采取拒绝策略
1.4 线程池处理流程
- 当线程池中的线程数小于corePoolSize (核心线程数) 时,有新提交的任务时,会创建一个新线程执行任务,即使线程池中仍有空闲线程
- 当线程池中的线程数达到corePoolSize (核心线程数) 时,新提交的任务将被放在workQueue (缓存队列) 中,等待线程池中的任务执行完毕
- 当workQueue (缓存队列) 满了,并且maximumPoolSize (最大线程数) 大于corePoolSize (核心线程数) 时,新提交任务时,会创建新的线程 (临时线程) 来执行任务
- 当任务数超过maximumPoolSize (最大线程数) 时,新任务就交给RejectedExecutionHandler (拒绝执行处理器) 来处理 ,执行相应的拒绝策略
- 当线程池线程数量中超过corePoolSize (核心线程数) 时,并且空闲时间达到keepAliveTime (空闲线程生存时间) 时,会关闭空闲线程
- 当设置allowCoreThreadTimeOut (允许核心线程超时) 参数为true时,线程池中核心线程的空闲时间达到keepAliveTime设置的值时,也会关闭超时的核心线程
1.5 线程池中阻塞队列
1.线程池中阻塞队列的作用?
- 一般的队列只能保证一个有限长度的缓冲区,如果超出了缓存长度,就无法保留当前的任务了;而阻塞队列通过阻塞可以保留当前想要继续入队的任务
- 阻塞队列可以保证任务队列中没有任务时,阻塞获取任务的线程,使线程进入wait状态,释放CPU资源
- 阻塞队列自带阻塞和唤醒的功能,不需要额外处理;无任务执行时,线程池就利用阻塞队列的take方法挂起,从而维持核心线程的存活,不至于一直占用CPU资源
2.为什么是先添加队列而不是先创建最大线程
- 在创建新线程时,是要获取全局锁的,这时其他线程就得阻塞,影响了整体效率
就好比一个企业里有10个正式员工的名额 (相当于核心线程数为10) ,最多招收15个员工 (相当于最大核心线程数为15),如果任务超过正式工人数 (即任务数大于核心线程数),老板 (相当于线程池) 不是首先扩招员工 (即创建新线程),而是将任务进行积压和推迟 (即先放到阻塞队列中去);
让10个员工先将手头工作做完后,然后再去执行推迟的任务 (这样做成本相对低一些),如果推迟的任务量超出了员工的处理能力范围 (即阻塞队列满了),但是老板 (线程池) 发现还有5个招收员工的名额 (相当于最大线程数大于核心线程数),因此老板开始招收临时工 (即创建临时线程) 来协助完成任务;
如果正式工加上临时工 (即核心线程加上临时线程) 还是无法完成任务,但是老板 (线程池) 发现公司已经没有资金再招收新员工了 (即阻塞队列已满并且线程池内线程数超过最大线程数),那么老板会拒绝接收新的任务 (即线程池执行拒绝策略)
1.6 为什么要禁止使用Executors创建线程池
- 因为Executors (执行器) 有newCachedThreadPool (创建可缓存线程池) 和newSingleThreadScheduledExecutor (创建定时单线程执行器) 这两个方法,它们的最大线程数为Integer.MAX_VALUE;如果达到上限,没有任务服务器可以继续工作,肯定会抛出OOM (OutOfMemoryExecution,内存泄露或溢出) 异常
//创建缓存线程池
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,ew SynchronousQueue<Runnable>());
}
//创建定时单线程执行器
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
- Executors (执行器) 还有另外两个方法:newSingleThreadExecutor (创建单线程执行器) 方法 和 newFixedThreadPool (创建定长线程池) 方法,它们的WorkQueue (工作队列>) 参数为new LinkedBlockingQueue () (LinkedBlockingQueue是用链表实现的有界阻塞队列) ,容量为Integer.MAX_VALUE,如果瞬间请求非常大,会有OOM (内存泄露) 风险
//创建单线程执行器
public static ExecutorService newSingleThreadExecutor() {
//new LinkedBlockingQueue<Runnable>()表示workQueue参数
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
//创建定长线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
//LinkedBlockingQueue(链表有界阻塞队列)的无参构造
public LinkedBlockingQueue() {
//最大线程数是:Integer.MAX_VALUE
this(Integer.MAX_VALUE);
}
//LinkedBlockingQueue的有参构造(参数为整型的capacity(容量)
public LinkedBlockingQueue(int capacity) {
//如果容量小于等于0.就抛出一个IllegalArgumentException(传递不正确参数异常)
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
//创建新节点,头尾值相等
last = head = new Node<E>(null);
}
注意:
- IllegalArgumentException:此异常表明向方法传递了一个不合法或者不正确的参数
- 以上五个核心方法newWorkStealingPool (创建工作窃取线程池) 之外,其它方法都有OOM风险
- newWorkStealingPool:一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用的CPU数量的线程来并发执行
1.7 线程池中线程复用原理
- 线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过Thread创建线程时,一个线程必须对应一个任务的限制
- 在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行
- 其核心原理在于,线程池对Thread进行了一个封装,并不是每次执行任务都会调用Thread.start()来创建新线程,而是让每个线程去执行一个“循环任务”
- 在这个“循环任务”中不停检查是否有任务需要被执行;如果有则直接执行,也就是调用任务中的run方法,将run方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的run方法串联起来
好了,今天有关线程池和线程池参数的学习就到此结束了,欢迎大家学习和讨论!
参考视频链接:
https://www.bilibili.com/video/BV1Eb4y1R7zd (B站UP主程序员Mokey的Java面试100道)