基本上所有的程序员都直接或间接使用过线程池。它是一把双刃剑,用好了事半功倍,用不好则可能直接打崩服务器。所以我们要对线程池有所了解,才能更好的去使用它。当然主要还是基于Java及SpringBoot。
1 线程池
1.1 为什么使用线程池
- 线程的创建和销毁是比较耗时的,利用池化技术可以减少这些消耗。
- 线程池一般是要维护几个核心线程等待任务去执行的,这样可以提高响应速度。
- 同时使用线程池也可以更方便的对线程进行管理,如控制数量、监控执行状态等。
1.2 如何创建线程池
在Java的juc
包中,有提供的Executors
类。一般情况为了使用方便,可能会直接使用该类的静态方法进行创建。而在这些静态方法中,都是基于ThreadPoolExecutor
类来构造的。下面我们分别来看一下。
1.2.1 ThreadPoolExecutor
线程池的重中之重,我们需要对它的各个参数和整体流程都十分熟悉才行。关于源码解析,我们后面单独用一篇博客来讲解。下面来看它参数最全的一个构造方法。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数解释:
- corePoolSize: 核心线程数。在线程池中要保持的线程数量,即使它是空闲的。除非设置
allowCoreThreadTimeOut
否则不会减少。可以调用prestartCoreThread
和prestartAllCoreThreads
来预先创建一个或全部核心线程。 - maximumPoolSize: 最大线程数。线程池中所允许的最大线程数量。
- keepAliveTime, unit: 存活时间。当线程数大于核心线程数时,若线程空闲时间大于该值将会被终止。
workQueue: 工作队列。阻塞队列,存储当前尚未被执行的任务。
- ArrayBlockingQueue: 基于数组的有界阻塞队列。
- LinkedBlockingQueue: 基于链表的阻塞队列,吞吐量通常高于ArrayBlockingQueue。
- SynchronousQueue: 不存储元素的阻塞队列,每个插入必须等待另一个线程调用移除操作
- PriorityBlockingQueue: 具有优先级的无限阻塞队列。
- threadFactory: 线程工厂。用于生成线程。主要设置是线程名称前缀,用于区分。这块一般会使用Google提供的
guava
包中的com.google.common.util.concurrent.ThreadFactoryBuilder
来构建。 handler: 拒绝策略。当当前线程池达到最大处理量时,则执行拒绝策略。这个其实就是标准的
策略模式
。- CallerRunsPolicy: 当前exector未关闭的情况下,由调用线程执行被拒绝的任务。
- AbortPolicy: 拒绝当前任务执行,并抛出
RejectedExecutionException
异常。 - DiscardPolicy: 直接丢弃当前任务,啥也不做。
- DiscardOldestPolicy: 当前exector未关闭的情况下,丢弃队列中最早的那个任务。
整体执行流程:
1.2.2 Executors内置静态方法
FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
特点:
- 核心线程数与最大线程数相同
- 存活时间:0。因为核心线程数和最大线程数相同,该参数无意义
- 阻塞队列为
LinkedBlockingQueue
可以认为是无界队列
当线程池线程数量达到最大线程数后,会将后续任务一直向阻塞队列压入。可能会触发OOM异常。
SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
特点:
- 几乎等同于
newFixedThreadPool(1)
- 实际返回为
FinalizableDelegatedExecutorService
private static class FinalizableDelegatedExecutorService
extends DelegatedExecutorService {
FinalizableDelegatedExecutorService(ExecutorService executor) {
super(executor);
}
@SuppressWarnings("deprecation")
protected void finalize() {
super.shutdown();
}
}
可见在finalize()
方法内,主动调用了super.shutdown()
。也就是当该执行器被垃圾回收时,会主动对实际执行器进行关闭。但还是建议当不使用时,及时主动关闭。
因为使用了LinkedBlockingQueue
可能会触发OOM异常。
newCachedThreadPool
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
特点:
- 核心线程数为0,最大线程数∞(可以认为是无穷大)
- 存活时间60秒
- 阻塞队列为
SynchronousQueue
,容量为0
每个提交的任务都会立即获得一个线程来执行,这样可能会造成大量的线程,从而触发OOM异常。
ScheduledThreadPool, SingleThreadScheduledExecutor
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1, threadFactory));
}
继承于ThreadPoolExecutor
扩展了其功能,可以使提交的任务延迟一定时间执行,或者周期执行。
WorkStealingPool
public static ExecutorService newWorkStealingPool(int parallelism) {
return new ForkJoinPool
(parallelism,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
在1.8中引入,使用ForkJoinPool
做为实现。该线程池会维持足够多的线程来支持给定的并行级别,可以使用多个队列来减少竞争。线程的实际数量可以动态的增加或减少。不能保证提交任务的执行顺序。
1.2.3 线程池提交任务
- execute: 常用的提交方式无返回值。
- submit: 适合需要有返回值的任务提交。会返回一个
Future
对象,可通过该对象判断任务是否执行并获取任务执行结果。
Future
在1.5中引入,在1.8中又引入了CompletableFuture
,关于CompletableFuture
后续再单独一篇讲一下。
1.2.4 线程池的关闭
- shutdownNow: 将当前线程池置为
STOP
状态,然后停止所有当前正在执行、暂停的任务,并返回任务待执行列表。 - shutdown: 将当前线程置为
SHUTDOWN
状态,中断没有在执行任务的线程,不再接受新任务,但之前提交的任务都会被执行。
1.3 线程池的监控
线程池本身提供了很多属性供获取:
- getPoolSize:线程池当前线程数
- getCorePoolSize:线程池核心线程数
- getActiveCount:正在执行任务的线程数量
- getCompletedTaskCount:已完成任务的大致总数 (任务和线程的状态可能在计算过程中有所变化)
- getTaskCount:全部任务的大致总数
- getQueue:当前线程池的任务队列
- getLargestPoolSize:线程池曾经最大线程数量
- getMaximumPoolSize:线程池允许最大线程数
- getKeepAliveTime:线程池线程存活时间
- isShutdown:线程池是否为关闭 (SHUTDOWN 状态)
- isTerminated:线程池是否为 TERMINATED 状态
同时线程池也提供了几个protected
空实现的方法。可以通过继承方式,重写beforeExecute(Thread t, Runnable r)
,afterExecute(Runnable r, Throwable t)
,terminated()
在扩展点出获取线程池状态。
1.4 配合Prometheus暴露监控指标
本例中的线程池,主要目的是想提高并发,但是并不想任务出现延迟,如果当前没有空闲线程来执行任务,则由自己去执行。
整体逻辑很简单,内部启用一个SingleThreadScheduledExecutor
每隔一定时间,收集线程池数据并通过io.micrometer.core.instrument.Metrics
对外暴露。
@Slf4j
@EnableAsync
@Configuration
public class ExecutorConfig implements InitializingBean, DisposableBean {
public static final String THREAD_POOL_NAME_FUTURE = "futureExecutor";
private static final ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
private static final Iterable<Tag> TAG = Collections.singletonList(Tag.of("thread.pool.name", THREAD_POOL_NAME_FUTURE));
@Bean(name = THREAD_POOL_NAME_FUTURE)
public Executor futureExecutor() {
return new ThreadPoolExecutor(
20,
80,
10,
TimeUnit.MINUTES,
new SynchronousQueue<>(),
new ThreadFactoryBuilder().setNameFormat(THREAD_POOL_NAME_FUTURE + "-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
@Override
public void afterPropertiesSet() {
scheduledExecutor.scheduleAtFixedRate(doCollect(), 30L, 5L, TimeUnit.SECONDS);
}
@Override
public void destroy() {
try {
scheduledExecutor.shutdown();
} catch (Exception ignored) {}
}
private Runnable doCollect() {
return () -> {
try {
ThreadPoolTaskExecutor exec = (ThreadPoolTaskExecutor) futureExecutor();
Metrics.gauge("thread.pool.core.size", TAG, exec, ThreadPoolTaskExecutor::getCorePoolSize);
Metrics.gauge("thread.pool.max.size", TAG, exec, ThreadPoolTaskExecutor::getMaxPoolSize);
Metrics.gauge("thread.pool.keepalive.seconds", TAG, exec, ThreadPoolTaskExecutor::getKeepAliveSeconds);
//
Metrics.gauge("thread.pool.active.size", TAG, exec, ThreadPoolTaskExecutor::getActiveCount);
Metrics.gauge("thread.pool.thread.count", TAG, exec, ThreadPoolTaskExecutor::getPoolSize);
//
Metrics.gauge("thread.pool.queue.size", TAG, exec, e -> e.getThreadPoolExecutor().getQueue().size());
//
Metrics.gauge("thread.pool.task.count", TAG, exec, e -> e.getThreadPoolExecutor().getTaskCount());
Metrics.gauge("thread.pool.task.completed.count", TAG, exec, e -> e.getThreadPoolExecutor().getCompletedTaskCount());
} catch (Exception ex) {
log.warn("doCollect ex => {}", ex.getLocalizedMessage());
}
};
}
}
1.5 SpringBoot中的线程池
在Spring项目中,经常会使用@EnableAsync
及@Async
来实现异步任务。默认情况下,会使用ThreadPoolTaskExecutor
。
public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport
implements AsyncListenableTaskExecutor, SchedulingTaskExecutor {
private final Object poolSizeMonitor = new Object();
private int corePoolSize = 1;
private int maxPoolSize = Integer.MAX_VALUE;
private int keepAliveSeconds = 60;
private int queueCapacity = Integer.MAX_VALUE;
private boolean allowCoreThreadTimeOut = false;
private boolean prestartAllCoreThreads = false;
默认拒绝策略为ThreadPoolExecutor.AbortPolicy()
。所以一般来说,我们都是需要自定义线程池的。
2 注意事项
2.1 阿里巴巴Java开发手册
【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
- 这个很好理解,像
http-nio-8103-exec-1
这种,一眼就能看出是http web相关线程
- 这个很好理解,像
【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
- 参考线程池的好处
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这 样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
2.2 其他
关于线程池中的,核心线程数、最大线程数、存活时间、及阻塞队列的选型及容量。没有啥固定的公式。
一般情况下, 会根据任务类型来做些区分
- CPU密集型任务。这种一般将线程数设置小一些,线程过多则上下文切换可能会耗时占比比较高。
- IO密集型任务。这种则尽量设置多一些的线程。可以大概预估请求等待时间 (WT) 和服务时间 (ST)之间的比例。线程池大小设置为 N*(1+WT/ST)。
不过最好还是做好指标监控,根据业务场景不断地调整优化。初始值可在测试环境等做些压测来确定。
3 后记
合理使用才能事半功倍,要始终抱着敬畏之心,防患于未然,做好指标监控。
又留俩坑:
- [ ] ThreadPoolExecutor源码
- [ ] CompletableFuture相关
echo '5Y6f5Yib5paH56ugOiDmjpjph5Eo5L2g5oCO5LmI5Zad5aW26Iy25ZWKWzkyMzI0NTQ5NzU1NTA4MF0pL+aAneWQpihscGUyMzQp' | base64 -d
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。