线程池的使用场景和工作原理

开发 前端
线程和进程进程就像是你打开的360安全卫士这个软件本身。线程:在这个空间里,360安全卫士可以运行它的各种功能,比如病毒扫描、垃圾清理等都是一个线程在处理。

 近期工作中遇到线程池参数配置不当引发的相关问题。基于此,本文主要结合线程池的原理、使用场景、参数配置、使用注意事项等详细阐述。文章较长,预计阅读时长8~10分钟,建议收藏。

图片图片

前置知识

  • 池化技术一种资源管理策略,旨在提高资源利用率和系统性能,减少资源创建和销毁的开销。如数据库连接池、线程池、常量池等。
  • 线程和进程进程就像是你打开的360安全卫士这个软件本身。线程:在这个空间里,360安全卫士可以运行它的各种功能,比如病毒扫描、垃圾清理等都是一个线程在处理。
  • 多线程在计算机中,CPU(中央处理器)是执行线程的地方。在多核CPU的系统中,每个核心可以独立执行线程。即多个线程可以真正地同时运行。

简单了解几个关键词,我们说说什么是线程池?

1.什么是线程池?

线程池是一种线程管理和复用的机制。

其核心思想是:预先创建一定数量的线程,并把它们保存在线程池中,用的时候拿出来,用完了丢进去。而不是每次有任务过来重新创建新的线程。如下图,其核心组成:

  • 工作线程(Worker):这些是线程池中实际执行任务的线程。
  • 任务队列(Task Queue):这是一个存放待执行任务的队列,工作线程会从这个队列中取出任务来执行。
  • 线程管理器(Thread Manager):负责管理线程的生命周期,包括创建新线程、监控线程状态以及销毁不再需要的线程。
  • 线程工厂(Thread Factory):用于创建新线程的组件,可以定制线程的名称、优先级等属性。
  • 拒绝策略(Rejection Policy):当任务队列满了,且工作线程都在忙碌时,新提交的任务将被拒绝,拒绝策略定义了如何处理这些被拒绝的任务。
  • 调度器(Scheduler):虽然不是所有线程池都有,但有些线程池会包含调度器,用于安排任务在特定时间执行。

架构图

图片图片

2.线程池有什么好处?

线程池的优势总结为以下几点:

  • 减少资源消耗:

线程池通过预先创建线程并复用它们,减少了频繁创建和销毁线程所带来的系统资源消耗。

  • 提高系统性能:

通过合理配置线程池大小,可以最大限度地压榨多核CPU的性能,从而提高机器的处理能力。

  • 提高响应速度:

线程池中的线程是预先创建的,这意味着当新任务到达时,可以立即被处理,而不需要等待线程的创建,这样可以显著提高任务的响应速度。

  • 增强并发性能:

线程池支持多线程并发执行任务,这样可以同时处理多个任务,增强系统的并发处理能力。

线程池有哪些使用场景?

线程池是一种多线程管理工具,它提供了线程的复用和调度功能,可以显著提高程序的并发性能和资源利用率。以下是线程池的一些常见使用场景:

  1. 文件上传下载

如:导入、导出功能、文件批数据处理等。

  1. 异步任务处理

如发送邮件、日志记录等.

  1. 定时任务

如定时备份、定时清理缓存等,日文件、月文件数据同步等

  1. 批处理

如数据分析、统计报表生成等。

  1. 快速响应用户请求

如用户查询商品详情页,可以考虑使用线程池并发地查询价格、优惠、库存等信息,再聚合结果返回,降低接口总响应时间。

线程池是如何工作的?

曾经有面试官这样问:核心线程数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 // 拒绝策略,当任务太多,无法被线程池及时处理时,采取的策略
  )

参数最佳实践:

实际上,大都数公司的线程池配置依照自身业务场景和机器性能配置的。这里介绍的只是个参考,能说清楚利弊就好

  1. 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
  1. maximumPoolSize          // 最大线程数

具体依据服务器的I/O性能,经验法则:

对于I/O密集型应用,通常将 maximumPoolSize 设置为可用处理器核心数的5到10倍。

文件处理服务:如果服务需要处理大量的文件上传和下载,maximumPoolSize 可以设置为20或更多。

  1. keepAliveTime          // 非核心线程空闲存活时间

看业务实时性高不高,一般系统,设置60s亦可

  1. unit          // 存活时间单位

看业务实时性高不高,一般系统,设置s亦可

  1. workQueue          // 工作队列

按目前经验,工作中常用:

LinkedBlockingQueue 是一个无界队列,适用于任务数量可能突然激增的场景;

ArrayBlockingQueue 是一个有界队列,适用于需要限制最大任务数量的场景,以避免资源耗尽

  1. threadFactory          // 线程工厂一般使用默认的也可
  2. 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;
   }

拒绝策略先前遇到坑,

篇幅,稍后单独做个介绍。

参考资料

责任编辑:武晓燕 来源: 码易有道
相关推荐

2021-06-06 23:40:53

线程池使用场景

2019-10-25 10:35:49

Java用法场景

2021-03-04 09:00:00

架构Lambda工具

2021-07-16 11:35:20

Java线程池代码

2022-05-06 13:30:56

TDD场景代码

2018-05-16 15:26:43

数据库MySQL主从复制

2024-03-11 18:18:58

项目Spring线程池

2021-08-29 22:05:04

对象自动回收

2010-12-21 14:21:36

线程C#

2018-05-22 09:47:07

2024-07-15 08:20:24

2018-09-19 14:53:02

NIOBIO运行

2023-05-16 07:47:18

RabbitMQ消息队列系统

2021-09-11 15:26:23

Java多线程线程池

2024-12-30 08:29:05

2024-04-11 13:41:47

2024-11-11 14:55:48

2020-10-29 07:16:26

布隆过滤器场景

2020-02-14 13:50:32

JavaScript前端技术

2012-05-15 02:18:31

Java线程池
点赞
收藏

51CTO技术栈公众号