面试被问线程池,真香
面试官 : 看你简历上写了对系统性能做了优化,能简单给我介绍一下吗?都有哪些优化,你是怎么衡量优化效果的?
我 : 巴拉巴拉。。。例如我们系统之前要查询用户的个人身份信息、联系人信息、订单状态信息、积分信息,之前系统是单线程串行处理的,我用线程池对四个任务并行处理,然后对处理结果合并。
面试官 : 你刚才说用到线程池,能跟我讲讲为什么用线程池吗?我创建四个线程处理可不可以?
我 : 可以,当然可以。
我 : 但是用线程池更合适。阿里巴巴开发规约中有一条:
3.【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
《阿里巴巴研发手册》
我 : 就像你去餐厅吃饭,服务员总是提前洗好盘子,不会等你来打饭的时候才洗盘子,盘子就像是线程池里的线程,你打饭就是要处理的任务。
面试官 : 那你知道线程池的相关类关系吗?
我: 这算什么问题?不应该是问我核心线程数怎么设置吗?好吧。。。请看下图:
-
Executor 的定义非常简单,就定义了线程池最本质要做的事,执行任务。
public interface Executor {
void execute(Runnable command);
}
-
ExecutorService 也是个接口,不过他算是把线程池的框架搭出来了,告诉要实现它的线程池必须提供的一些管理线程池的方法。 -
AbstractExecutorService 是普通的线程池执行器,ScheduledExecutorService 是定时任务线程池。
ThreadPoolExecutor
自定义创建线程池。
ThreadPoolExecutor
构造函数就知道了
-
corePoolSize :核心线程数 -
maximumPoolSize: 最大线程数 -
keepAliveTime :线程在线程池中不被销毁的空闲时间,如果线程池的线程太多,任务比较小,到这个时间就销毁线程池。 unit : keepAliveTime 的时间单位,一般设置成秒或毫秒。 -
workQueue : 任务队列,存放等待执行的任务 -
threadFactory: 创建线程的任务工厂,比如给线程命名加上前缀,后面会讲 -
handler : 拒绝任务处理器,当任务处理不过来时的拒绝处理器 -
allowCoreThreadTimeOut : 是否允许核心线程超时销毁,这个参数不在构造函数中,但重要性也很高
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数目 -
也有开源的辅助测算线程池的合理线程数。
-
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);
}
}
有道无术,术可成;有术无道,止于术
好文章,我在看❤️