击穿线程池面试题:3大方法,7大参数,4种拒绝策略

前言:多线程知识是Java面试中必考的点。本文详细介绍——线程池。在实际开发过程里,很多IT从业者使用率不高,也只是了解个理论知识,和背诵各种八股文,没有深入理解到脑海里,导致面试完就忘。——码农 = 敲代码;程序员= 理解

线程池面试必考点:3大方法,7大参数,4种拒绝策略!

目录

▶ 介绍

一 . 线程池(Thread Pool)

二 . Executor、Executors 、ExecutorService 别再傻傻分不清!

▶ 逐一击穿

一 . 3大方法

二 . 7大参数

三 . 4种拒绝策略

▶ 最后


介绍

一 . 线程池(Thread Pool)

程序运行的本质就是:占用系统资源! 资源的竞争就会影响程序的运行,势必要优化资源的利用。例如:池化技术的诞生!常见的有:Java中的对象池、内存池、jdbc连接池、线程池等等

池化技术的原理:事先准备好资源,有人要用,就来我这里拿,用完再还给我!

我们知道创建、销毁线程十分浪费资源,不仅产生额外开销,还降低了计算机性能。使用线程池来管理线程,大白话总结就是:线程可复用,可控制最大并发数,线程方便管理

  • 减少线程频繁创建和销毁的开销!
  • 避免线程数过大导致过分调度cpu问题!
  • 提高了响应速度!

是为了最大化收益并最小化风险,而将资源统一在一起管理

 
  

二 . Executor、Executors 、ExecutorService 别再傻傻分不清!

Executors 工厂工具类   

是一个工具类, 提供工厂方法来创建不同类型的线程池供大家使用!

要什么样的线程池就new什么线程池给你,相当于一个工厂!!

该工厂提供的常见的线程池类型:

// 可缓存的线程池:工作线程如空闲了60秒将会被回收。终止后如有新任务加入,则重新创建一条线程
Executors.newCachedThreadPool(); 

// 固定线程数池:工作线程<核心线程数,则新建线程执行任务;工作线程=核心线程数,新任务则放入阻塞队列
Executors.newFixedThreadPool(); 

// 单线程池:只有一条线程执行任务,按照指定顺序(FIFO,LIFO)执行,新加入的任务放入阻塞队列(串行)
Executors.newSingleThreadExecutor(); 
  
// 定时线程池:支持定时及周期性任务执行
Executors.newScheduledThreadPool(); 

 Executor 执行者接口   

线程池的顶级接口,其他类都直接或间接实现它!只提供一个execute()方法,用来执行提交的Runnable任务——只是一个执行线程的工具类!!

public interface Executor {
    void execute(Runnable command);
}

ExecutorService 线程池接口 

线程池接口,继承Executor且扩展了Executor【执行者接口】,能够关闭线程池,提交线程获取执行结果,控制线程的执行。可以理解为:执行者接口Executor的升级版本!

public interface ExecutorService extends Executor {
    //温柔的终止线程池,不再接受新的任务,对已提交到线程池中任务依旧会处理
    void shutdown();

    //强硬的终止线程池,不再接受新的任务,同时对已提交未处理的任务放弃,并返回
    List shutdownNow();

    //判断当前线程池状态是非运行状态,已执行shutdown()或shutdownNow()
    boolean isShutdown();

    //线程池是否已经终止
    boolean isTerminated();

    //阻塞当前线程,等待线程池终止,支持超时
    boolean awaitTermination(long timeout, TimeUnit unit)
            throws InterruptedException;
    ...
}

扩展:下表列出了 Executor 和 ExecutorService 的区别

Executor ExecutorService
Java 线程池的核心接口,用来并发执行提交的任务 是 Executor 接口的扩展子接口,提供了异步执行和关闭线程池的方法
提供execute()方法用来提交任务 提供submit()方法用来提交任务
无返回值 submit()方法返回Future对象,可用来获取任务执行结果
不能取消任务 可通过Future.cancel()取消pending中的任务
不支持关闭线程池 提供了关闭线程池的方法

 逐一击穿

一 . 3大方法

指的是Executors工厂类提供的3种线程池。

上述讲解Executors工厂类说明了4种创建方式,这里只展示3种考的最多的!

a. 单线程池 

public static void main(String[] args) {
     // 1.定义一个单线程池:池中只有1条线程
     ExecutorService pool = Executors.newSingleThreadExecutor(); 

     // 2.定义5条线程
     for (int i = 0; i < 5; i++) {
          pool.execute(()->{
               System.out.println(Thread.currentThread().getName()+" hi");
          });
     }

     // 3.关闭线程池
     pool.shutdown();
}
执行结果:从头到尾只有1条线程,且执行了5个任务

 击穿线程池面试题:3大方法,7大参数,4种拒绝策略_第1张图片

b. 可缓存的线程池 

public static void main(String[] args) {
        /** 说明:池中最大线程的创建数量为:Integer.MAX_VALUE(约等于21亿)
                 如果线程超过60秒没执行任务,会自动回收该线程,译为可缓存的池子 */
        // 1.定义一个可缓存的线程池
        ExecutorService pool =Executors.newCachedThreadPool(); 

        // 2.定义5条线程
        for (int i = 0; i < 5; i++) {
            pool.execute(()->{
                System.out.println(Thread.currentThread().getName()+" hi");
            });
        }

        // 3.关闭线程池
        pool.shutdown();
}

执行结果:池中被创建了5条线程执行5个任务,线程最大能创建多少条,取决于你的电脑性能

击穿线程池面试题:3大方法,7大参数,4种拒绝策略_第2张图片

c. 固定线程池 

public static void main(String[] args) {
   
        // 1.定义一个固定长度为3的线程池
        ExecutorService pool =Executors.newFixedThreadPool(3); 

        // 2.定义5条线程
        for (int i = 0; i < 5; i++) {
            pool.execute(()->{
                System.out.println(Thread.currentThread().getName()+" hi");
            });
        }

        // 3.关闭线程池
        pool.shutdown();
}

执行结果:池中定义了3个线程,所以执行任务的线程最多只有3条

击穿线程池面试题:3大方法,7大参数,4种拒绝策略_第3张图片

考点分析:线程池为什么不允许使用 Executors 工厂类 去创建!!

答:规避资源耗尽的风险。弊端如下:(不能理解的话,见下面7大参数的讲解)

  • FixedThreadPool 和 SingleThreadPool:阻塞队列的任务容量为 Integer.MAX_VALUE (约21亿),会堆积大量的请求导致 OOM,造成系统瘫痪。

  • CachedThreadPool 和 ScheduledThreadPool:最大创建线程数量为 Integer.MAX_VALUE,创建大量的线程易导致 OOM。

快速记忆:固定或单一长度的线程池,队列容量没限制!非固定的池,创建线程数没限制!


 

二 . 7大参数

指的是自定义线程池中的7个设置参数(重点)

要点扩展:首先来查看下Executors工厂类提供的 3大方法的源码分析

// 固定长度线程池:nThreads为传入参数,最大线程数和核心线程数都为此值,固定线程数长度
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue());
}

// 单线程池:之所以只有1条线程执行,是因为核心线程数和最大线程数都是1
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue()));

// 可缓存的线程池:最大创建数没有限制,设置了60秒空闲时间,空闲的线程超过此时间将会被回收
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue());
}

我们可以看到上述3个方法中,实际底层创建线程池都用到同一个 new ThreadPoolExecutor(),ThreadPoolExecutor是JUC提供的一类线程池工具,从字面含义来看,是指管理一组同构工作线程的资源池。通常理解为是一个自定义线程池。

来看下该类的构造参数说明:

/** 
      corePoolSize:核心线程数
   maximumPoolSize:可创建的最大线程数
     keepAliveTime:存活时间,超过此time没任务执行的线程会被回收
              unit:存活时间的单位
     BlockingQueue:阻塞队列
     ThreadFactory:线程工厂,创建线程的,一般不用
   RejectedExecutionHandler:拒绝策略
*/
public ThreadPoolExecutor(int corePoolSize, 
                          int maximumPoolSize, 
                          long keepAliveTime, 
                          TimeUnit unit,
                          BlockingQueue workQueue,
                          ThreadFactory threadFactory, 
                          RejectedExecutionHandler handler) {
       ...
}

corePoolSize 核心线程数量

  • 默认情况只有当新任务到达时,才会创建和启动核心线程,可用 prestartCoreThread()启动一个核心线程 和 prestartAllCoreThreads() 启动所有核心线程的方法动态调整
  • 即使没任务执行,核心线程也会一直存活
  • 池内的线程数小于核心线程时,即使有空闲线程,线程池也会创建新的核心线程执行任务
  • 设置allowCoreThreadTimeout=true时,核心线程会超时关闭

maximumPoolSize 最大线程数

  • 当所有核心线程都在执行任务,且任务队列已满时,线程池会创建新非核心线程来执行任务
  • 当池内线程数=最大线程数,且队列已满,再有新任务会触发RejectedExecutionHandler策略

keepAliveTime TimeUnit 线程空闲时间

  • 如果线程数>核心线程数,线程的空闲时间达到keepAliveTime时,线程会被回收销毁,直到线程数量=核心线程
  • 如果设置allowCoreThreadTimeout=true时,核心线程执行完任务也会销毁直到数量=0

workQueue 任务队列

  • 队列的说明请参考 :

ThreadFactory 创建线程的工厂

  • 一般用来自定义线程名称,线程多的时候,可以分组区分识别

RejectedExecutionHandler 拒绝策略

  • 最大线程数和队列都满的情况下,对新任务的处理方式,请参考下方的4种策略

上述参数的说明太过于理论化,下面我将用生活的例子来说明重点参数的使用

模拟银行办理业务流程

击穿线程池面试题:3大方法,7大参数,4种拒绝策略_第4张图片

  1. 银行就相当于一个线程池
  2. 银行内部最多有5个窗口可以办理业务( maximumPoolSize 最大线程创建数),只有前2个窗口正在办理业务中(corePoolSize 核心线程数),另外3个窗口处于暂停办理
  3. 等候区只有3个位置提供排队(workQueue队列容量为:3),并且已经排满了人

由图可得出的线程池参数为:

  • corePoolSize 核心线程数:2
  • maximumPoolSize 最大线程数:5
  • workQueue 任务队列大小:3

结论:如果核心窗口满了,则新来办理业务的人会进入排号等候区等待(阻塞队列)

思考:核心窗口和等候区都满了,那如果此时银行进来2个新办理业务的人呢?

击穿线程池面试题:3大方法,7大参数,4种拒绝策略_第5张图片

 由图可知,新进来2个人流程如下:

  1. 如果核心窗口被占用中(核心线程)则判断等候区 (阻塞队列)是否满了
  2. 排号区没满,判断等候区(阻塞队列)是否还有位置,如果有则进入等候区排队等待。
  3. 排号区和核心窗口都满了 (核心线程数+阻塞队列容量),那么就去判断银行的全部窗口(最大线程创建数)是否都在办理业务,如果没有,就开放2个新的窗口给新进来的2人办理业务(非核心窗口)

结论:核心线程数2 + 阻塞队列3 全部已满,如果工作线程还没达到最大线程数,线程池会给新进来的2个任务,开放创建2个新的非核心窗口来处理新任务

思考:那如果此时银行再来2个办理业务的人呢?

击穿线程池面试题:3大方法,7大参数,4种拒绝策略_第6张图片

由图可知:

  1. 窗口5还没满,所以会被开放给新进来的其中一个人办理业务
  2. 而另外一个人,因5个窗口已全被占用,且等候区也满了,会被拒绝办理(拒绝策略)

总结,线程池的运行原理如下:

击穿线程池面试题:3大方法,7大参数,4种拒绝策略_第7张图片

  1. 核心线程没满时,即便池中线程都处于空闲,也创建新核心线程来处理新任务。

  2. 核心线程数已满,但阻塞队列 workQueue未满,则新进来的任务会放入队列中。

  3. 核心线程数和阻塞队列都满了,如果当前线程数< 最大线程创建数, 会创建新 (非核心)线程来处理新任务

  4. 如三者都满了(核心线程数、阻塞队列、最大线程数)则通过指定的拒绝策略处理任务

优先级为:核心线程corePoolSize、任务队列workQueue、(非核心线程) 最大线程maximumPoolSize,如三者都满了,采用handler拒绝策略


三 . 4种拒绝策略

指的是线程池中的最大线程数和队列都满的情况下,对新进来任务的处理方式

  • CallerRunsPolicy(调用者运行策略):使用当前调用的线程 (提交任务的线程) 来执行此任务

  • AbortPolicy(中止策略):拒绝并抛出异常 (默认)

  • DiscardPolicy(丢弃策略):丢弃此任务,不会抛异常

  • DiscardOldestPolicy(弃老策略):抛弃队列头部(最旧)的一个任务,并执行当前任务

下面我将用代码来进行演示说明:

 CallerRunsPolicy(调用者运行策略)

/**
      corePoolSize核心线程数:2
   maximumPoolSize最大线程数:3
       workQueue阻塞队列容量:2
    RejectedHandler拒绝策略:CallerRunsPolicy 调用者运行
*/
public static void main(String[] args) {
        // 1.自定义一个线程池
        ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 3, 0
                , TimeUnit.SECONDS
                ,new LinkedBlockingQueue(2)
                , new ThreadPoolExecutor.CallerRunsPolicy()); // 指定拒绝策略

        // 2.提交6个任务
        for (int i = 0; i < 6; i++) {
            final int num =i;
            pool.execute(()->{
                System.out.println(Thread.currentThread().getName()+" ok:"+num);
            });
        }

        // 3.关闭线程池
        pool.shutdown();
    }

执行结果如下:

击穿线程池面试题:3大方法,7大参数,4种拒绝策略_第8张图片

说明:首先提交了6个任务,而线程池可接收的线程容量为:队列2 + 最大创建线程数3 = 5个,因为最大线程创建数为3,所以最多只有3个线程去轮询执行5个任务,多余的第6个任务,线程池因为占满了故没办法运行,线程池指定的拒绝策略是:调用者运行策略  也就是提交任务的线程去处理,即main线程

AbortPolicy(中止策略)

与上述调用者策略的代码一致,修改ThreadPoolExecutor后面的具体策略类型即可

new ThreadPoolExecutor.AbortPolicy(); // 指定拒绝策略

执行结果如下:

击穿线程池面试题:3大方法,7大参数,4种拒绝策略_第9张图片

说明:由于采用的是中止策略,拒绝任务并抛出异常,第6个任务因线程池已满,而无法执行。

DiscardPolicy(丢弃策略)

new ThreadPoolExecutor.DiscardPolicy(); // 指定拒绝策略

执行结果如下:

击穿线程池面试题:3大方法,7大参数,4种拒绝策略_第10张图片

 说明:第6个任务被丢弃了,结束程序运行

DiscardOldestPolicy(弃老策略)

new ThreadPoolExecutor.DiscardOldestPolicy(); // 指定拒绝策略

执行结果如下:

击穿线程池面试题:3大方法,7大参数,4种拒绝策略_第11张图片

说明:首先核心线程是2个,故任务1、2直接进入线程池被核心线程执行,而任务3进来后,核心线程已满,则进入队列等待,任务4随后也进入队列,任务5进来后,因为核心线程和队列都已满,但还没有达到最大线程创建数3,故会创建一条非核心线程去处理任务5。此时池中已达到最大线程数,而队列也占满了,最后任务6进来,所以会抛弃最早进入队列的任务3

 最后

  1. 本文没有很深入的讲解很多源码知识,只是带着读者理解:线程池是什么?怎么用?
  2. 面试的必考点知识,基本上都是围绕着:3大方法、7大参数、4种拒绝策略 来问问题,如果你能够读懂本篇文章,基本上已经掌握了线程池的知识点!
  3. 面试造火箭,上班拧螺丝不可耻!死记硬背八股文才可耻,只有真正理解在自己脑海里的知识才是有所收获,否则你看的每一篇文章都只是别人的笔记。面试前背,面试完就忘,坚决拒绝短暂的记忆,带着自己的方式去理解它,这才是我想要表达的观点!

优秀的判断力来自经验,但经验来自于错误的判断。

你可能感兴趣的:(并发编程,java,线程池,并发编程)