面试官 : 看你简历上写了对系统性能做了优化,能简单给我介绍一下吗?都有哪些优化,你是怎么衡量优化效果的?
我 : 巴拉巴拉。。。例如我们系统之前要查询用户的个人身份信息、联系人信息、订单状态信息、积分信息,之前系统是单线程串行处理的,我用线程池对四个任务并行处理,然后对处理结果合并。
面试官 : 你刚才说用到线程池,能跟我讲讲为什么用线程池吗?我创建四个线程处理可不可以?
我 : 可以,当然可以。
摆手
我 : 但是用线程池更合适。阿里巴巴开发规约中有一条:
3.【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
《阿里巴巴研发手册》
我 : 就像你去餐厅吃饭,服务员总是提前洗好盘子,不会等你来打饭的时候才洗盘子,盘子就像是线程池里的线程,你打饭就是要处理的任务。
面试官 : 那你知道线程池的相关类关系吗?
我: 这算什么问题?不应该是问我核心线程数怎么设置吗?好吧。。。请看下图:
- 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
创建线程池,可以理解原理,灵活度更高。
参考阿里开发手册规约:
4.【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors返回的线程池对象的弊端如下:
1)FixedThreadPool
和SingleThreadPool
:
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
2)CachedThreadPool
:
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。《阿里巴巴研发手册》
面试官 : 前面你代码里有任务入队的操作,你一般自定义线程池,用的什么队列?
我: 这个要看实际应用的。
- 有的任务在早上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); } }
面试官 : 那这几种拒绝策略,你选哪一种?
我: 我选拒绝回答
面试官 : 我选你回去等通知。
有道无术,术可成;有术无道,止于术
欢迎大家关注Java之道公众号