面试官 : 看你简历上写了对系统性能做了优化,能简单给我介绍一下吗? 都有哪些优化,你是怎么衡量优化效果的?
我 : 巴拉巴拉。。。例如我们系统之前要查询用户的个人身份信息、联系人信息、订单状态信息、积分信息,之前系统是单线程串行处理的,我用线程池对四个任务并行处理,然后对处理结果合并。
面试官 : 你刚才说用到线程池,能跟我讲讲为什么用线程池吗? 我创建四个线程处理可不可以?
我 : 可以,当然可以。
我 : 但是用线程池更合适。阿里巴巴开发规约中有一条:
我 : 就像你去餐厅吃饭,服务员总是提前洗好盘子,不会等你来打饭的时候才洗盘子,盘子就像是线程池里的线程,你打饭就是要处理的任务。
面试官 : 那你知道线程池的类结构吗?
我: 这算什么问题? 不应该是问我核心线程数怎么设置吗?好吧。。。请看下图:
- Executor 的定义非常简单,就定义了线程池最本质要做的事,执行任务。
public interface Executor {
void execute(Runnable command);
}
ExecutorService 也是个接口,不过他算是把线程池的框架搭出来了,告诉要实现它的线程池必须提供的一些管理线程池的方法。
AbstractExecutorService 是普通的线程池执行器,ScheduledExecutorService 是定时任务线程池。
面试官 : 那你日常开发中是怎么创建线程池的?
我: 我用ThreadPoolExecutor
自定义创建线程池。
面试官 : 那你知道线程池创建时都有哪些参数吗?
我: 线程池主要的核心参数有7个,我们看 ThreadPoolExecutor
构造函数就知道了
corePoolSize :核心线程数
maximumPoolSize: 最大线程数
keepAliveTime :线程在线程池中不被销毁的空闲时间,如果线程池的线程太多,任务比较小,到这个时间就销毁线程池。
unit : keepAliveTime 的时间单位,一般设置成秒或毫秒。
workQueue : 任务队列,存放等待执行的任务
threadFactory: 创建线程的任务工厂,比如给线程命名加上前缀,后面会讲
handler : 拒绝任务处理器,当任务处理不过来时的拒绝处理器
allowCoreThreadTimeOut : 是否允许核心线程超时销毁,这个参数不在构造函数中,但重要性也很高
面试官 : 老实说,你是不是来之前背过了,不然怎么可能都记住了。
我: [掀桌子],不面了,还找什么工作,要什么自行车。
我不过是来之前把“安琪拉的博客”公众号上的文章都看了个遍。
面试官 : 其实刚才那也是问题,考察面试者是否皮实,我们继续。。
面试官 : 刚才说了这些核心参数,你能不能跟我讲讲线程池的基本工作原理。
我: 可以的,这里我给你画个流程,如下所示:
面试官 : 那按照上面的流程写段伪代码。
我: 还能不能好好面了,让手撕线程池。
那好吧,你对着👆🏻的流程图看,代码如下:
面试官 : 不错,那你平常怎么管理线程池的呢?
我: 我会搞了个线程池管理器,比如 ThreadPoolManager,有个私有变量的Map,按照线程池的作用给他取个名字,比如起名为: preparePlateThreadPool (准备餐盘线程池),把线程池名称定义成常量,和创建好的线程池放到管理器的Map里。
面试官 : 除了你自己用 ThreadPoolExecutor
创建线程池,还有别的方式吗?
我: java.util.concurrent
包里提供的 Executors
也可以用来创建线程池。
面试官 : Executors
定义了哪几种 ?
我:
- newSingleThreadExecutos 单线程线程池,也就是线程池只有一个任务,这个我偶尔用一用
- newFixedThreadPool(int nThreads) 固定大小线程的线程池
- newCachedThreadPool() 无界线程池,这个就是无论多少任务,都创建线程来运行,所以队列相当于没用。
面试官 : 你上面讲日常开发自己 用 ThreadPoolExecutor
创建线程池,为什么不用Executors
提供的。
我: 第一是 Executors
提供的线程池使用场景很有限,一般场景很难用到,第二他们也都是通过 ThreadPoolExecutor
创建的线程池,我直接用 ThreadPoolExecutor
创建线程池,可以理解原理,灵活度更高。
参考阿里开发手册规约:
面试官 : 前面你代码里有任务入队的操作,你一般自定义线程池,用的什么队列?
我: 这个要看实际应用的。
- 有的任务在早上8点和晚上6点都是高峰期,因此有任务尖刺,用
LinkedBlockingQueue
, 这个是无界队列,不限制任务大小的。 - 对于重要性没那么高,非强依赖的任务用的
ArrayBlockingQueue
,这个是指定大小的,如果任务超出,会创建非核心线程执行任务。
面试官 : 那你怎么保证任务队列的可用性呢?
我: 分几个方面:
- 我的线程池管理器,会有一个定时任务,定时检测Map 中线程池当前任务队列的状态,会设置一个 waterThreshold(水位线),超出水位线会有告警;
- 日常大促演练,会对线程池做压测,如果发生超水位情况,还会对线程按线程名做降级,动态调整核心线程数和队列,当然还有限流、降级等其他有段保障。
面试官 : 那你怎么合理拆分线程池,核心任务数和任务队列大小的呢?
我: 这个是个老生常谈的问题。
按照任务的类型,对任务做拆分,分成不同的线程池,分别命名;
区分任务的类型,是CPU密集型还是IO密集型,CPU 可以设置约为CPU核心数,上下文切换少,io密集型可以设置的大一些。
大体估算一个,然后做压测,评估,另外线程池有个变量也可以参考意义: largestPoolSize,线程池达到过的最大线程任务,比如你刚开始可以把线程数设置的足够大,压测过后看这个参数达到的最大数值,同时参考系统的性能指标,cou、io、mem等。
这里还有个公式借鉴: 最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
也有开源的辅助测算线程池的合理线程数。
面试官 : 那拒绝策略呢? 了解吗
我: 拒绝策略就是当任务太多,超过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); } }
面试官 : 那这几种拒绝策略,你选哪一种?
我: 我选拒绝回答
面试官 : 我选你回去等通知。