本文主要介绍:
- 什么是线程池?
- 如何理解线程池的构造方法
- 使用Executors创建线程池有什么问题?
- 正确创建线程池的姿势
在阿里巴巴Java开发手册中,有一条强制规则,不允许使用Executors去创建线程池,为什么会有这条规则呢?带着这个问题,今天我们来一起探究一下Java线程池的工作原理
首先抛出一个问题,什么是线程池?
从线程池的定义上来讲,线程池(Thread Pool)是一种基于池化思想的线程管理工具,过多的线程会带来额外的开销,比如创建或销毁线程、线程调度等。线程池通过维护多个线程,来避免这种开销,同时也在一定程度上避免了线程数量的膨胀。
从使用上来讲,线程池实现的是线程的复用,每提交一个任务,这个任务会被分配给一个线程去执行,任务执行完,会将线程归还至线程池
如何理解线程池的构造方法?
我们可以来看一下用 new TheadPoolExecutor 方法创建线程池的参数
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // Keep Alive 时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 工作队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler) // 拒绝策略
图片摘自《Java线程池实现原理及其在美团业务中的实践》,强烈建议大家去读一下原文。
这里我们先入为主的给出线程池执行任务的步骤,大家可以结合上的图示,对照一下。线程池的设计,符合生产消费模型
- 线程池初始化后,不会立刻创建线程,而是等到第一个任务提交后,才创建线程
- 线程创建的个数如果大于CorePoolSize,任务就会被放到工作队列中暂存起来
- 工作队列存满后,创建线程至maximumPoolSize个线程
- 线程数已经创建至maximumPoolSize个线程,仍没有空闲线程消费任务,新提交上来的任务会根据RejectedExecutionHandler(拒绝策略),进行拒绝操作
- 当前线程数大于核心线程数时,线程等待KeepAliveTime后,仍没有任务提交上来,则认为线程空闲,线程池会收缩线程至corePoolSize
有时间的同学可以阅读一下线程池的源码,对照着上面的步骤,看一下代码层面上是如何实现的
所有的拒绝策略都实现了RejectedExecutionHandler 这个接口
AbortPolicy: 直接抛出 RejectedExecutionException
CallerRunsPolicy: 由提交任务的线程去执行
DiscardOldestPolicy: 丢弃工作队列中等待时间最长的任务, 后将任务提交至工作队列
DiscardPolicy: 什么都不做, 直接将任务丢弃
那么回到问题本身,使用Executors创建线程池有什么问题?
FixedThreadPool / SingleThreadPool 的工作队列是无界的,大量任务堆积,容易产生OOM
CacheThreadPool / ScheduledThreadPool 可以创建的线程的个数是无界的,创建过多的线程会导致OOM或者是 cpuload飙升
通常我们可能存在侥幸心理,认为这种情况基本不会发生,但是在高并发的场景下,这种情况却很容易出现,比如我们依赖下游服务,QPS为100,假设我们客户端的超时时间为1分钟,由于网络抖动,下游服务有1min的时间不可用,这个时间我们的请求会被hang住,1min的时间会产生 6000个请求,如果是CacheThreadPool异步调用,我们会在1min之内创建6000个线程,很容易使服务OOM
正确创建线程池的姿势
这里抛砖引玉,给出一个通过guava ThreadFactory创建线程池的demo,仅做参考
@Slf4j
class Solution {
private static final int CORE_POOL_SIZE = 10;
private static final int MAX_POOL_SIZE = 150;
private static final int BLOCKING_QUEUE_SIZE = 10;
private static final long KEEP_ALIVE_TIME = 10L;
private static final ThreadFactory guavaThreadFactory =
new ThreadFactoryBuilder().setNameFormat("thread-pool-%d").build();
private static final ExecutorService exec = new ThreadPoolExecutor(CORE_POOL_SIZE,
MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(BLOCKING_QUEUE_SIZE), guavaThreadFactory);
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
exec.submit(() -> log.info("is working"));
}
}
}
创建线程池的时候,需要做到核心功能和非核心功能分开始用,配置不同的参数,避免相互影响
感谢阅读、欢迎交流
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。