面试官 : 看你简历上写了对系统性能做了优化,能简单给我介绍一下吗? 都有哪些优化,你是怎么衡量优化效果的?

: 巴拉巴拉。。。例如我们系统之前要查询用户的个人身份信息、联系人信息、订单状态信息、积分信息,之前系统是单线程串行处理的,我用线程池对四个任务并行处理,然后对处理结果合并。

面试官 : 你刚才说用到线程池,能跟我讲讲为什么用线程池吗? 我创建四个线程处理可不可以?

: 可以,当然可以。

面试被问线程池,真香-LMLPHP

: 但是用线程池更合适。阿里巴巴开发规约中有一条:

: 就像你去餐厅吃饭,服务员总是提前洗好盘子,不会等你来打饭的时候才洗盘子,盘子就像是线程池里的线程,你打饭就是要处理的任务。

面试被问线程池,真香-LMLPHP

面试官 : 那你知道线程池的类结构吗?

: 这算什么问题? 不应该是问我核心线程数怎么设置吗?好吧。。。请看下图:

面试被问线程池,真香-LMLPHP

  • Executor 的定义非常简单,就定义了线程池最本质要做的事,执行任务。
public interface Executor {

    void execute(Runnable command);
}
  • ExecutorService 也是个接口,不过他算是把线程池的框架搭出来了,告诉要实现它的线程池必须提供的一些管理线程池的方法。

  • AbstractExecutorService 是普通的线程池执行器,ScheduledExecutorService 是定时任务线程池。

面试官 : 那你日常开发中是怎么创建线程池的?

: 我用ThreadPoolExecutor 自定义创建线程池。

面试官 : 那你知道线程池创建时都有哪些参数吗?

: 线程池主要的核心参数有7个,我们看 ThreadPoolExecutor 构造函数就知道了

面试被问线程池,真香-LMLPHP

  1. corePoolSize :核心线程数

  2. maximumPoolSize: 最大线程数

  3. keepAliveTime :线程在线程池中不被销毁的空闲时间,如果线程池的线程太多,任务比较小,到这个时间就销毁线程池。

    unit : keepAliveTime 的时间单位,一般设置成秒或毫秒。

  4. workQueue : 任务队列,存放等待执行的任务

  5. threadFactory: 创建线程的任务工厂,比如给线程命名加上前缀,后面会讲

  6. handler : 拒绝任务处理器,当任务处理不过来时的拒绝处理器

  7. allowCoreThreadTimeOut : 是否允许核心线程超时销毁,这个参数不在构造函数中,但重要性也很高

面试官 : 老实说,你是不是来之前背过了,不然怎么可能都记住了。

: [掀桌子],不面了,还找什么工作,要什么自行车。

面试被问线程池,真香-LMLPHP

​ 我不过是来之前把“安琪拉的博客”公众号上的文章都看了个遍。

面试官 : 其实刚才那也是问题,考察面试者是否皮实,我们继续。。

面试官 : 刚才说了这些核心参数,你能不能跟我讲讲线程池的基本工作原理。

: 可以的,这里我给你画个流程,如下所示:

面试被问线程池,真香-LMLPHP

面试官 : 那按照上面的流程写段伪代码。

: 还能不能好好面了,让手撕线程池。

面试被问线程池,真香-LMLPHP

那好吧,你对着👆🏻的流程图看,代码如下:

面试被问线程池,真香-LMLPHP

面试官 : 不错,那你平常怎么管理线程池的呢?

: 我会搞了个线程池管理器,比如 ThreadPoolManager,有个私有变量的Map,按照线程池的作用给他取个名字,比如起名为: preparePlateThreadPool (准备餐盘线程池),把线程池名称定义成常量,和创建好的线程池放到管理器的Map里。

面试官 : 除了你自己用 ThreadPoolExecutor 创建线程池,还有别的方式吗?

: java.util.concurrent 包里提供的 Executors 也可以用来创建线程池。

面试官 : Executors 定义了哪几种 ?

:

  • newSingleThreadExecutos 单线程线程池,也就是线程池只有一个任务,这个我偶尔用一用
  • newFixedThreadPool(int nThreads) 固定大小线程的线程池
  • newCachedThreadPool() 无界线程池,这个就是无论多少任务,都创建线程来运行,所以队列相当于没用。

面试官 : 你上面讲日常开发自己 用 ThreadPoolExecutor 创建线程池,为什么不用Executors 提供的。

: 第一是 Executors 提供的线程池使用场景很有限,一般场景很难用到,第二他们也都是通过 ThreadPoolExecutor 创建的线程池,我直接用 ThreadPoolExecutor 创建线程池,可以理解原理,灵活度更高。

参考阿里开发手册规约:

面试官 : 前面你代码里有任务入队的操作,你一般自定义线程池,用的什么队列?

: 这个要看实际应用的。

  1. 有的任务在早上8点和晚上6点都是高峰期,因此有任务尖刺,用 LinkedBlockingQueue, 这个是无界队列,不限制任务大小的。
  2. 对于重要性没那么高,非强依赖的任务用的ArrayBlockingQueue,这个是指定大小的,如果任务超出,会创建非核心线程执行任务。

面试官 : 那你怎么保证任务队列的可用性呢?

: 分几个方面:

  1. 我的线程池管理器,会有一个定时任务,定时检测Map 中线程池当前任务队列的状态,会设置一个 waterThreshold(水位线),超出水位线会有告警;
  2. 日常大促演练,会对线程池做压测,如果发生超水位情况,还会对线程按线程名做降级,动态调整核心线程数和队列,当然还有限流、降级等其他有段保障。

面试官 : 那你怎么合理拆分线程池,核心任务数和任务队列大小的呢?

: 这个是个老生常谈的问题。

  1. 按照任务的类型,对任务做拆分,分成不同的线程池,分别命名;

  2. 区分任务的类型,是CPU密集型还是IO密集型,CPU 可以设置约为CPU核心数,上下文切换少,io密集型可以设置的大一些。

  3. 大体估算一个,然后做压测,评估,另外线程池有个变量也可以参考意义: largestPoolSize,线程池达到过的最大线程任务,比如你刚开始可以把线程数设置的足够大,压测过后看这个参数达到的最大数值,同时参考系统的性能指标,cou、io、mem等。

  4. 这里还有个公式借鉴: 最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

  5. 也有开源的辅助测算线程池的合理线程数。

面试官 : 那拒绝策略呢? 了解吗

: 拒绝策略就是当任务太多,超过maximumPoolSize了,只能拒绝。

面试官 : 详细讲讲

: 拒绝的时候可以指定拒绝策略,也可以自己实现,JDK默认提供了四种拒绝策略.

  • AbortPolicy

    默认拒绝策略, 直接抛RejectedExecutionException

  • DiscardPolicy

    任务直接丢弃,不抛出异常

  • CallerRunsPolicy

    由调用者来执行被拒绝的任务,比如主线程调用线程池的submit提交任务,但是任务被拒绝,则主线程直接执行。

    但是线程池如果已经被关闭了,任务就被丢弃了。

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
      //线程池没关闭
      if (!e.isShutdown()) {
      //直接run,没有让线程池来执行
        r.run();
      }
    }
    
  • DiscardOldestPolicy

    丢弃队列里等的最久的任务,然后尝试执行被拒绝的任务。

    但是线程池如果已经被关闭了,任务就被丢弃了

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
      if (!e.isShutdown()) {
        //丢弃队列头部任务
        e.getQueue().poll();
        //线程池尝试执行任务
        e.execute(r);
      }
    }
    

面试官 : 那这几种拒绝策略,你选哪一种?

: 我选拒绝回答

面试官 : 我选你回去等通知。

02-06 22:11