JUC并发编程基础之线程池和线程池参数

前言

在面试过程中,我们可能会被面试官经常问到有关线程池线程池参数的相关问题,如果对于这些问题,你的心中没有明确的答案,那么在看完本篇博客后,相信你将会有所收获!

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 线程池处理流程

JUC并发编程基础之线程池和线程池参数_第1张图片

  • 当线程池中的线程数小于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()表示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道)

你可能感兴趣的:(JUC并发编程学习,juc,线程池,阻塞队列,面试,java)