当前位置 博文首页 > 最爱吃鱼罐头:每日三道面试题,通往自由的道路12——线程池

    最爱吃鱼罐头:每日三道面试题,通往自由的道路12——线程池

    作者:[db:作者] 时间:2021-08-29 10:13

    茫茫人海千千万万,感谢这一秒你看到这里。希望我的面试题系列能对你的有所帮助!共勉!

    愿你在未来的日子,保持热爱,奔赴山海!

    每日三道面试题,成就更好自我

    昨天既然聊到线程池中的实现方式,有些比较重要的我还没问到。

    1. 你知道ThreadPoolExecutor的构造方法和参数吗

    我们先来看看它的构造方法有哪些:

    // 五个参数的构造函数
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {...}
    
    // 六个参数的构造函数
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {...}
    
    // 六个参数的构造函数
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) {...}
    
    // 七个参数的构造函数
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {...}
    

    我们再来详解下构造方法中涉及的7个参数,其中最重要5个参数就是第一个构造方法中的。

    • int corePoolSize:该线程池中核心线程数量

      核心线程:线程池中有两类线程,核心线程和非核心线程。核心线程默认情况下会一直存在于线程池中,即使这个核心线程什么都不干,而非核心线程(临时工)如果长时间的闲置,就会被销毁。但是如果将

      allowCoreThreadTimeOut设置为true时,核心线程也是会被超时回收。

    • int maximumPoolSize:该线程池中允许存在的工作线程的最大数量。

      该值相当于核心线程数量 + 非核心线程数量。

    • long keepAliveTime:非核心线程闲置超时时长。

      非核心线程如果处于闲置状态超过该值,就会被销毁。如果设置allowCoreThreadTimeOut(true),则会也作用于核心线程。

    • TimeUnit unit:keepAliveTime的时间单位。

      TimeUnit是一个枚举类型 ,包括以下属性:

      NANOSECONDS : 1微毫秒 
      MICROSECONDS : 1微秒
      MILLISECONDS : 1毫秒
      SECONDS : 秒 
      MINUTES : 分
      HOURS : 小时
      DAYS : 天
      
    • BlockingQueue workQueue:阻塞队列,维护着等待执行的Runnable任务对象。

      当新任务来的时候,会先判断当前运行线程数量是否达到了核心线程数,如果达到了,就会被存放在阻塞队列中排队等待执行。

      常用的几个阻塞队列:

      1. ArrayBlockingQueue

        数组阻塞队列,底层数据结构是数组,需要指定队列的大小。

      2. SynchronousQueue

        同步队列,内部容量为0,每个put操作必须等待一个take操作,反之亦然。

      3. DelayQueue

        延迟队列,该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素 。

      4. LinkedBlockingQueue

        链式阻塞队列,底层数据结构是链表,默认大小是Integer.MAX_VALUE,也可以指定大小。

    还有两个非必须的参数:

    • ThreadFactory threadFactory

      创建线程的工厂 ,用于批量创建线程,统一在创建线程时设置一些参数,如是否守护线程、线程的优先级等。如果不指定,会新建一个默认的线程工厂。

    • RejectedExecutionHandler handler

      拒绝处理策略,在线程数量大于最大线程数后就会采用拒绝处理策略,四种拒绝处理的策略为 :

      1. ThreadPoolExecutor.AbortPolicy默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException异常。
      2. ThreadPoolExecutor.DiscardPolicy:丢弃新来的任务,但是不抛出异常。
      3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列头部(最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程)。
      4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。

    不错呀!线程池的参数也有深入了解,那咱们继续

    2. 你可以说下线程池的执行过程原理吗

    昨天MyGirl跟我讲了一下她去银行办理业务的一个场景:

    1. 首先MyGirl(任务A)先去银行(线程池)办理业务,她发现她来早了,现在银行才刚开门,柜台窗口服务员还没过来(相当于线程池中的初始线程为0),此时银行经理看到MyGirl来了,就安排她去一号柜台窗口并安排了1号正式工作人员来接待她。
    2. 在MyGirl的业务还没办完时,一个不知名的路人甲(任务B)出现了,他也是要来银行办业务,于是银行经理安排他去二号柜台并安排了2号正式工作人员。假设该银行的柜台窗口就只有两个(核心线程数量2)。
    3. 紧接着,在所有人业务都还没做完的情况,持续来个三个不知名的路人乙丙丁,他们也是要来办业务的,但是由于柜台满了,安排了他们去旁边的银行大厅的座位上(阻塞队列,这里假设大小为3)等候并给了对应顺序的号码,说等前面两个人办理完后,按顺序叫号你们呦,请注意听。
    4. 过一会,一个路人戊也想来银行办理业务,而经理看到柜台满了,座位满了,只能安排了一个临时工(非核心线程,这里假设最大线程为3,即非核心为1)手持pad设备并给路人戊去办理业务。
    5. 而此时,一个路人戌过来办理业务,而经理看到柜台满了,座位满了,临时工也安排满了(最大线程数+阻塞队列都满了),无奈经理只能掏出一本《如何接待超出最大限度的手册》,选择拒接接待路人戌通知他,过会再来吧您嘞,这里已经超负荷啦!
    6. 最后,相继所有人的业务都办完了,现在也没人再来办业务,并且临时工的空闲时间也超过了1小时以上了(最大空闲时间默认60秒),经理让临时工都先下班回家了(销毁线程)。
    7. 但是一个银行要保证正常的运行,只能让正式员工继续上班,不得提早下班。

    而实际上线程的流程原理跟这个一样,我们来看下处理任务的核心方法execute,它的源码大概是什么样子的呢,当然我们也可以看源码中的注释,里面也写的很清楚。这里具体讲下思路。

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();  
        // 1. 获取ctl,ctl是记录着线程池状态和线程数。
        int c = ctl.get();
        // 2. 判断当前线程数小于corePoolSize核心线程,则调用addWorker创建核心线程执行任务
        if (workerCountOf(c) < corePoolSize) {
           if (addWorker(command, true))
               return;
           // 创建线程失败,需要重新获取clt的状态和线程数。
           c = ctl.get();
        }
        // 3. 如果不小于corePoolSize,进入下面的方法。
        // 判断线程池是否运行状态并且运行线程数大于corePoolSize,将任务添加到workQueue队列。
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            // 3.1 再次检查线程池是否运行状态。
            // 如果isRunning返回false(状态检查),则remove这个任务,然后执行拒绝策略。
            if (! isRunning(recheck) && remove(command))
                reject(command);
                // 3.2 线程池处于running状态,但是没有线程,则创建线程加入到线程池中
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        // 4. 如果放入workQueue失败,则创建非核心线程执行任务,
        // 如果这时创建非核心线程失败(当前线程总数不小于maximumPoolSize时),就会执行拒绝策略。
        else if (!addWorker(command, false))
             reject(command);
    }
    

    我们可以大概看下思路图:

    先解释下ctl

    变量ctl定义为AtomicInteger,记录了“线程池中的任务数量”和“线程池的状态”两个信息。以高三位记录着线程池的状态和低29位记录线程池中的任务数量。

    RUNNING : 111
    SHUTDOWN : 000
    STOP : 001
    TIDYING : 010
    TERMINATED : 011
    

    最后总结一下执行过程:

    1. 任务到达时,会先判断核心线程是否满了,不满则调用addWorker方法创建核心线程执行任务。
    2. 然后会判断下线程池中的线程数 < 核心线程,无论线程是否空闲,都会新建一个核心线程执行任务(让核心线程数量快速达到核心线程总数)。此步骤会开启锁mainLock.lock();
    3. 而在线程池中的线程数 >= 核心线程时,新来的线程任务会进入任务阻塞队列中等待,然后空闲的核心线程会依次去阻塞队列中取任务来执行。
    4. 当阻塞队列满了,说明这个时候任务很多了,此时就需要一些非核心线程临时工来执行这些任务了。于是会创建非核心线程去执行这个任务。
    5. 最后当阻塞队列满了, 且总线程数达到了maximumPoolSize,则会采取拒绝策略进行处理。
    6. 当非核心线程取任务的时间达到keepAliveTime还没有取到任务即空闲时间,就会回收非核心线程。

    不错,这个执行过程原理都有深入了解过,最后问你一道:

    3. 能否写一个简单线程池的demo?

    你这怕不是魔鬼吧,写一个线程池。不过简单的线程池还是可以写写滴!当然通过上面参数,执行过程的学习,写出来一个还是比较So Easy的。只是如果真的到面试了,真的让你手敲,可能就忘了,还是得多敲。

    这里还是直接用简单的ThreadPoolExecutor创建吧,等后续写线程池相关文章,再详细写自己创建的线程池吧。

    我们先创建一个任务类Task:

    /**
     * 自定义任务类
     */
    public class Task implements Runnable{
    
        private int id;
    
        public Task(int id) {
            this.id = id;
        }
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "即将执行的任务是" + id + "任务");
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "执行完成的任务是" + id + "任务");
        }
    }
    

    测试代码:

    public class ThreadPoolExecutorDemo {
    
        private static final int CORE_POOL_SIZE = 3;
        private static final int MAX_POOL_SIZE = 5;
        private static final int QUEUE_CAPACITY = 10;
        private static final Long KEEP_ALIVE_TIME = 1l;
    
    
        public static void main(String[] args) {
            //通过ThreadPoolExecutor构造函数自定义参数创建
            ThreadPoolExecutor executor = new ThreadPoolExecutor(
                    CORE_POOL_SIZE,
                    MAX_POOL_SIZE,
                    KEEP_ALIVE_TIME,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                    new ThreadPoolExecutor.CallerRunsPolicy());
    
            for (int i = 0; i < 10; i++) {
                Task task = new Task( i);
                //执行Runnable
                executor.execute(task);
            }
            //终止线程池
            executor.shutdown();
            while (!executor.isTerminated()) {
            }
            System.out.println("线程已经全部执行完");
        }
    }
    

    得到的结果:

    pool-1-thread-1即将执行的任务是0任务
    pool-1-thread-3即将执行的任务是2任务
    pool-1-thread-2即将执行的任务是1任务
    pool-1-thread-1执行完成的任务是0任务
    pool-1-thread-3执行完成的任务是2任务
    pool-1-thread-1即将执行的任务是3任务
    pool-1-thread-3即将执行的任务是4任务
    pool-1-thread-2执行完成的任务是1任务
    pool-1-thread-2即将执行的任务是5任务
    pool-1-thread-3执行完成的任务是4任务
    pool-1-thread-1执行完成的任务是3