线程池(三) 面试题

面试问题

  1. 线程池主要参数
    线程池的主要参数为:核心线程数量,最大线程数量,线程空闲存活时间数,线程空闲存活时间单位,缓存队列, 线程创建工厂(默认Executors.defaultThreadFactory()),拒绝策略(默认AbortPolicy拒绝策略); 其中缓存队列有:ArrayBlockingQueue(有界队列),LinkedBlockingDeque(无界队列),SynchronousQueue(该队列不存数据) 还有一个参数:allowCoreThreadTimeOut,该参数作用是设置核心线程是否也和非核心线程一样有空闲存活时间;

  2. 线程池执行流程
    提交任务,判断线程数量是否超过核心线程数量,未超过则创建新线程来执行任务,超过则加入队列;如果队列已满, 则创建新线程处理任务,直到线程达到最大线程数;如果达到最大线程数量,则执行拒绝策略;

  3. 线程池的存活时间
    正常情况下,线程池创建了是可以一直存活的,除非创建线程池的主线程或者整个程序不再运行;除非手动调用线程池的关闭方法 (shutdown()或shutdownNow()),或者线程池运行出现直接导致线程池无法处理的错误;

  4. 线程池是如何维持线程一直运行
    线程池中的线程,使用while方法一直循环的去队列获取任务;在没有设置allowCoreThreadTimeOut的情况下: 核心线程数之内的线程会使用阻塞式方式从队列中获取任务(所以线程会一直运行挂起), 核心线程数之后的线程会使用超时策略从队列中获取任务,当超时且队列中没有任务时,当前线程就会跳出while循环(就是线程执行完毕,线程会被回收) ,继续执行后续操作,直到执行完毕; 当设置allowCoreThreadTimeOut后,所有的线程都是使用超时方式从队列中获取任务,直到线程池中只剩下一个有效线程;

  5. 关闭线程池 有两个方法来关闭线程池: shutdown():停止接收新的任务(Runnable),剩下的任务依然会继续执行; shutdownNow():停止接收任务,不处理队列中的任务,执行中的线程也会被终止;

  6. 线程池如何对多余的线程进行回收的
    这个问题就要回到线程池如何维持线程一直运行的这个地方了,当线程池队列中没得任务了,就退出runWorker中while循环了, 最后就进入了processWorkerExit这个方法,当这个方法执行完毕后,也就意味着这个线程的任务运行结束了,也就标志着这个线程执行完毕了;这个方法主要就是将原线程执行的任务数添加到线程池中,并且将worker中移除该worker;

  7. 线程池如何保证线程安全的 几乎所有的方法都用了ReentrantLock锁来保证线程安全;而线程池对ReentrantLock进行了实例化,也就是一个线程池中的所有线程共享这个ReentrantLock锁; 还有一个就是在addWorker方法中,前半部分使用cas来保证修该线程池中的线程数量是安全的,后半部分又使用ReentrantLock锁来保证将worker加入workers的线程安全; 还有一个就是在getTask方法中,并发情况下对线程进行回收时,也是使用的CAS来进行并发控制的(CAS对ctl进行操作);

  8. 几种常用的线程池,简单说一下
    • newSingleThreadExecutor:核心线程和最大线程数都为1,使用LinkedBlockingQueue队列,且限定容量为Integer.MAX_VALUE,确保按照指定顺序执行
    • newFixedThreadPool:固定线程数量的线程池,核心线程数和最大线程数是一致的;也是使用LinkedBlockingQueue队列,且限定容量为:Integer.MAX_VALUE;
    • newCachedThreadPool:创建一个核心线程数为0,最大线程数为Integer.MAX_VALUE的线程池,且使用SynchronousQueue队列,也就是进入一个任务就新建一个线程来处理该任务;
    • newScheduledThreadPool:底层是ScheduledThreadPoolExecutor,该类继承了ThreadPoolExecutor,主要是执行定时或周期性的任务;

    上面的几个线程池都是通过Executors去创建的,要么就是队列容量设置为:Integer.MAX_VALUE,要么就是最大线程数量设置为:Integer.MAX_VALUE,这是会出现资源耗尽的风险的,但是并不建议使用Executors去创建线程池,而是手动创建ThreadPoolExecutor,规避资源耗尽的风险;

  9. 线程池中的异常处理
    这些异常分为两类:一类就是线程池自身异常,基本上都是线程池对一些自有属性及状态进行检查以及传入线程池的参数检查而直接抛出的异常;另一类就是传入线程的任务在执行过程中抛出的异常了;

    这里说一下执行任务出错,这里也分两种情况:1. 如果是直接使用的execute方法提交的任务,那么在ThreadPoolExecutor的runWorker方法中,使用try对任务的执行进行了包装,然后将任务及抛出的异常作为参数去调用afterExecute(task, thrown),而线程池中的这个方法默认是没有任何处理的;2. 如果使用ThreadPoolExecutor继承的AbstractExecutorService类的submit方法提交的任务,就使用了FutureTask来对任务进行了封装,这时看向FutureTask的run方法,也使用try对任务运行出错进行了包装,所以在最终在ThreadPoolExecutor的runworker中就无法捕捉任务执行抛出的错误,注意:上面所说的try并不是捕捉的所有的错误,需要看代码出的具体错误及catch的具体错误;

    那自己又怎么去处理异常呢:1. 在任务中使用try catch来封装,但是这样就只能在任务内部处理了;2.如果使用submit提交的任务,使用Future的get方法来获取抛出的异常;3. 重写线程池中的afterExecute方法,自己对任务和错误进行处理;

  10. execute和submit的区别?
    execute是ThreadPoolExecutor自己的方法,是直接对任务进行处理的,这是没有返回值的;
    submit方法是ThreadPoolExecutor继承的类AbstractExecutorService类里面的方法,这个方法使用FutureTask对任务进行了封装,然后再使用封装后的任务作为参数调用execute方法,而且submit是可以有返回值的;

  11. 线程池有多个线程同时没取到任务,会全部回收吗? 不会,因为默认情况下allowCoreThreadTimeOut为false,如果回收了,那么剩下的线程就不满足设置的核心线程数了,且allowCoreThreadTimeOut也无作用了; 为什么不会被回收,因为在getTask的方法中,使用了CAS来进行了并发控制,保证了多线程的情况下只有一个线程会成功被回收;

  12. 拒绝策略有哪些?
    • CallerRunsPolicy:使用调用线程池的线程(原线程)来处理任务
    • AbortPolicy:抛出RejectedExecutionException错误,这是线程池默认拒绝策略
    • DiscardPolicy:不做任何处理,空方法,就是舍弃任务;
    • DiscardOldestPolicy:舍弃队列中的老任务,然后再调用execute方法,注意这里的任务并不是直接进入的队列,经过execute来处理的;
      首先拒绝策略需要实现RejectedExecutionHandler接口;上面的四种拒绝策略都是定义在ThreadPoolExecutor类中的静态类;

最后

持续更新中。。。

参考

  1. 如何合理配置线程池的大小
  2. java线程池 面试题(精简)
  3. 面试必备:Java线程池解析
  4. 5 年 Java 经验,字节、美团、快手核心部门面试总结(真题解析)
坚持原创技术分享,您的支持将鼓励我继续创作!