线程池提供一种限制和管理资源(包括执行一个任务)。每个线程池还维护一些基本统计信息,例如已完成任务的数量
线程池的好处:
- 降低资源的消耗
- 提高相应速度
- 提高线程的可管理性
线程池在实际项目的使用场景
线程池一般用于执行多个不相关联的耗时任务,没有多线程的情况下,任务顺序执行,使用了线程池可以让多个不想关联的任务同时执行
假如要执行三个不想管的耗时任务。下面三个任务可能是同一件事,也可能是不一样的事
如何使用线程池
ThreadPoolExecutor的构造函数来创建线程池,然后将任务提交给线程池执行
/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
int maximumPoolSize,//线程池的最大线程数
long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
线程池最佳实践
使用ThreadPoolExecutor构造函数声明线程池
- 线程池必须手动通过ThreadPoolExecutor的构造函数声明,避免使用Executors类的newFixedThreadPool和newCachedThreadPool。可能会有OOM风险
Executors返回线程池对象的弊端:
- FixedThreadPool 和SingleThreadExecutor:允许请求的队列长度为Integer.MAX_VALUE,可能堆积大量请求,从而导致OOM
- CachedThreadPool 和 ScheduledThreadPool:允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量线程,从而导致OOM
使用有界队列,控制创建线程数量
除了避免OOM之外,不推荐Executors提供的 两种快捷线程池的原因还有:
- 实际使用中需要根据自己机器的性能、业务场景来手动配置线程池的参数,比如核心线程数,使用的任务队列,饱和策略等
- 应该显示地给线程池命名,有助于定位问题
监测线程池运行状态
SpringBoot中的Actuator组件可以来检测线程池的运行状态
还可以利用ThreadPoolExecutor的相关API做简陋监控。ThreadPoolExecutor提供获取线程当前线程数和活跃线程数、已执行完的任务数,正在排队中的任务数等等。
demo:printThreadPoolStatus():每隔一秒打印出线程池的线程数、活跃线程数、完成的任务数、以及队列中的任务数
/**
* 打印线程池的状态
*/
public static void printThreadPoolStatus(ThreadPoolExecutor threadPool) {
ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1, createThreadFactory("print-images/thread-pool-status", false));
scheduledExecutorService.scheduleAtFixedRate(() -> {
log.info("=========================");
log.info("ThreadPool Size: [{}]", threadPool.getPoolSize());
log.info("Active Threads: {}", threadPool.getActiveCount());
log.info("Number of Tasks : {}", threadPool.getCompletedTaskCount());
log.info("Number of Tasks in Queue: {}", threadPool.getQueue().size());
log.info("=========================");
}, 0, 1, TimeUnit.SECONDS);
}
不同类别的业务使用不同的线程池
不同的业务使用不同的线程池,配置线程池的时候根据当前业务的情况对当前线程池进行配置,因为不同的业务的并发以及对资源的使用情况都不同,重心优化系统性能瓶颈相关的业务。
上面的代码可能会存在死锁情况
极端情况
假设线程池的核心线程数为n,父任务(扣费任务)数量为n,父任务下面有两个子任务(扣费任务下的子任务),其中一个已执行完成,另外一个被放在任务队列中。由于父任务把线程池核心线程资源用完,所以子任务因为无法获取线程资源无法正常执行,一直被阻塞在队列中。父任务等待子任务执行完成,而子任务等待父任务释放线程,造成死锁。
解决方法:新增一个用于执行子任务的线程专门为其服务
线程池命名
初始化线程池时需要显示命名(设置线程池名称前缀),有利于定位问题
默认情况下,创建的线程名类似pool-1-thread-n这样,没有业务含义,不利于定位问题
给线程池的线程命名通常有两种方式
- 利用guava的ThreadFactoryBuilder
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat(threadNamePrefix + "-%d").setDaemon(true).build(); ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory);
- 自己实现ThreadFactory
import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; /** * 线程工厂,它设置线程名称,有利于我们定位问题。 */ public final class NamingThreadFactory implements ThreadFactory { private final AtomicInteger threadNum = new AtomicInteger(); private final ThreadFactory delegate; private final String name; /** * 创建一个带名字的线程池生产工厂 */ public NamingThreadFactory(ThreadFactory delegate, String name) { this.delegate = delegate; this.name = name; // TODO consider uniquifying this } @Override public Thread newThread(Runnable r) { Thread t = delegate.newThread(r); t.setName(name + " [#" + threadNum.incrementAndGet() + "]"); return t; } }
正确配置线程池参数
上下文切换:任务从保存到再加载的过程就是一次上下文切换。当前任务在执行完CPU时间片切换到另外一个任务之前先保存自己的状态,以便下次再切换回这个任务,可以再加载这个任务的状态。
- CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
- I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
参考阅读
- 本文链接:https://wentianhao.github.io/2021/08/25/%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%AE%9E%E8%B7%B5/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。
若没有本文 Issue,您可以使用 Comment 模版新建。