近期工作中遇到线程池参数配置不当引发的相关问题。基于此,本文主要结合线程池的原理、使用场景、参数配置、使用注意事项等详细阐述。文章较长,预计阅读时长8~10分钟,建议收藏。
图片
前置知识
- 池化技术一种资源管理策略,旨在提高资源利用率和系统性能,减少资源创建和销毁的开销。如数据库连接池、线程池、常量池等。
- 线程和进程进程就像是你打开的360安全卫士这个软件本身。线程:在这个空间里,360安全卫士可以运行它的各种功能,比如病毒扫描、垃圾清理等都是一个线程在处理。
- 多线程在计算机中,CPU(中央处理器)是执行线程的地方。在多核CPU的系统中,每个核心可以独立执行线程。即多个线程可以真正地同时运行。
简单了解几个关键词,我们说说什么是线程池?
1.什么是线程池?
线程池是一种线程管理和复用的机制。
其核心思想是:预先创建一定数量的线程,并把它们保存在线程池中,用的时候拿出来,用完了丢进去。而不是每次有任务过来重新创建新的线程。如下图,其核心组成:
- 工作线程(Worker):这些是线程池中实际执行任务的线程。
- 任务队列(Task Queue):这是一个存放待执行任务的队列,工作线程会从这个队列中取出任务来执行。
- 线程管理器(Thread Manager):负责管理线程的生命周期,包括创建新线程、监控线程状态以及销毁不再需要的线程。
- 线程工厂(Thread Factory):用于创建新线程的组件,可以定制线程的名称、优先级等属性。
- 拒绝策略(Rejection Policy):当任务队列满了,且工作线程都在忙碌时,新提交的任务将被拒绝,拒绝策略定义了如何处理这些被拒绝的任务。
- 调度器(Scheduler):虽然不是所有线程池都有,但有些线程池会包含调度器,用于安排任务在特定时间执行。
架构图
图片
2.线程池有什么好处?
线程池的优势总结为以下几点:
- 减少资源消耗:
线程池通过预先创建线程并复用它们,减少了频繁创建和销毁线程所带来的系统资源消耗。
- 提高系统性能:
通过合理配置线程池大小,可以最大限度地压榨多核CPU的性能,从而提高机器的处理能力。
- 提高响应速度:
线程池中的线程是预先创建的,这意味着当新任务到达时,可以立即被处理,而不需要等待线程的创建,这样可以显著提高任务的响应速度。
- 增强并发性能:
线程池支持多线程并发执行任务,这样可以同时处理多个任务,增强系统的并发处理能力。
线程池有哪些使用场景?
线程池是一种多线程管理工具,它提供了线程的复用和调度功能,可以显著提高程序的并发性能和资源利用率。以下是线程池的一些常见使用场景:
- 文件上传下载
如:导入、导出功能、文件批数据处理等。
- 异步任务处理
如发送邮件、日志记录等.
- 定时任务
如定时备份、定时清理缓存等,日文件、月文件数据同步等
- 批处理
如数据分析、统计报表生成等。
- 快速响应用户请求
如用户查询商品详情页,可以考虑使用线程池并发地查询价格、优惠、库存等信息,再聚合结果返回,降低接口总响应时间。
线程池是如何工作的?
曾经有面试官这样问:核心线程数10,最大线程数20,阻塞队列最大100,假如我有100个线程同时进来,线程处理任务时间平均按1s计算,那理想情况下多长时间可以处理完?
基于上边的问题,线程池的工作原理可以概括为以下几个步骤:
- 工作原理(简易版)
图片
- 任务提交:
开发人员使用 ThreadPoolExecutor 的 submit() 方法提交需要执行的任务。这些任务通常是实现了 Callable 或 Runnable 接口的对象。
- 状态检查:
线程池会检查自身的运行状态。如果线程池不是处于 RUNNING 状态,那么会直接拒绝新提交的任务。
- 任务封装:
被提交的任务会被封装成一个 FutureTask 对象。FutureTask 实现了 Future 接口,用来获取任务的执行结果。
- 核心线程处理:
如果线程池的核心线程数小于 corePoolSize,线程池会尝试创建一个新的核心线程来执行这个任务。
- 任务队列处理:
如果核心线程数已经达到 corePoolSize,则任务会被放入一个任务队列中,等待工作线程从队列中取出并执行。
- 非核心线程处理:
如果任务队列已满,并且当前线程池中的线程数量小于 maximumPoolSize,则线程池会尝试创建新的非核心线程来执行任务。
- 拒绝策略处理:
如果线程池中的线程数量已经达到 maximumPoolSize,并且任务队列也已满,线程池将根据设定的拒绝策略来处理新提交的任务。
- 任务执行与结果获取:
任务执行完成后,线程池会返回一个 Future 对象。通过这个 Future 对象,可以查询任务是否完成,以及获取任务的执行结果。
总之,归纳起来的执行流程:
图片
线程池参数如何设置?
《阿里巴巴Java开发手册》中规约中强调:
图片
因此实际工作中,我们通常自定义线程池。其核心参数:
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数,线程池中始终保持的线程数,即使它们处于空闲状态
int maximumPoolSize, // 最大线程数,线程池中允许的最大线程数
long keepAliveTime, // 非核心线程空闲存活时间,当线程池中正在运行的线程数量超过了核心线程数时,多余的线程在空闲时间达到这个值后会被终止
TimeUnit unit, // 存活时间单位,与keepAliveTime一起使用,表示keepAliveTime的时间单位
BlockingQueue<Runnable> workQueue, // 工作队列,用于存放待执行任务的阻塞队列
ThreadFactory threadFactory, // 线程工厂,用于创建新线程
RejectedExecutionHandler handler // 拒绝策略,当任务太多,无法被线程池及时处理时,采取的策略
)
参数最佳实践:
实际上,大都数公司的线程池配置依照自身业务场景和机器性能配置的。这里介绍的只是个参考,能说清楚利弊就好
- corePoolSize, // 核心线程数
- CPU密集型可以将线程数设置为 N(CPU 核数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断。
- IO密集型系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法可以是 :核心线程数=CPU核数*2。
如何理解 CPU 密集 和I/O密集?
CPU密集型任务通常在CPU上执行进行大量计算(如RSA加密)I/O密集多指I/O操作(主要磁盘读写,如 数据库操作等)
附加:如何获取CPU核数?
代码:
Runtime.getRuntime().availableProcessors();
命令:
$ lscpu
- maximumPoolSize // 最大线程数
具体依据服务器的I/O性能,经验法则:
对于I/O密集型应用,通常将 maximumPoolSize 设置为可用处理器核心数的5到10倍。
文件处理服务:如果服务需要处理大量的文件上传和下载,maximumPoolSize 可以设置为20或更多。
- keepAliveTime // 非核心线程空闲存活时间
看业务实时性高不高,一般系统,设置60s亦可
- unit // 存活时间单位
看业务实时性高不高,一般系统,设置s亦可
- workQueue // 工作队列
按目前经验,工作中常用:
LinkedBlockingQueue 是一个无界队列,适用于任务数量可能突然激增的场景;
ArrayBlockingQueue 是一个有界队列,适用于需要限制最大任务数量的场景,以避免资源耗尽
- threadFactory // 线程工厂一般使用默认的也可
- handler // 拒绝策略
主要有4种拒绝策略:
AbortPolicy:直接丢弃任务,抛出异常,这是默认策略
CallerRunsPolicy:只用调用者所在的线程来处理任务
DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执行当前任务
DiscardPolicy:直接丢弃任务,也不抛出异常
介于此,给出公司中用到的其中一个案例配置,仅供参考。
@Bean("XXXAsyncPool")
public Executor myTaskAsyncPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//配置核心线程数
executor.setCorePoolSize(10);
//配置核心线程数
executor.setMaxPoolSize(20);
//配置队列容量
executor.setQueueCapacity(1000);
//设置线程活跃时间
executor.setKeepAliveSeconds(60);
//设置线程名
executor.setThreadNamePrefix("XXXAsyncPool-");
//设置拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
拒绝策略先前遇到坑,
篇幅,稍后单独做个介绍。