线程池

线程池

线程池就是一种池化技术,用于预先创建并管理一组线程,避免频繁创建和销毁线程的开销,提高性能和响应速度。

他的几个关键配置包括:核心线程、最大线程数、空闲存活时间、工作队列、拒绝策略

五大线程池、七个参数、四个拒绝策略

线程池相关参数解释

  • corePoolSize核心线程数,即线程池中始终保持的线程数量。
  • maximumPoolSize最大线程数,即线程池中允许的最大线程数量。
  • keepAliveTime:线程空闲时间,超过这个时间的非核心线程会被销毁。
  • workQueue任务队列,存放待执行的任务。
  • threadFactory:线程工厂,用于创建新线程。
  • rejectedExecutionHandler:任务拒绝处理器,当任务无法执行时的处理策略。

工作队列类型

  • SynchronousQueue:不存储任务,直接将任务提交给线程。
  • LinkedBlockingQueue:链表结构的阻塞队列,大小无限。
  • ArrayBlockingQueue:数组结构的有界阻塞队列。
  • PriorityBlockingQueue:带优先级的无界阻塞队列。

主要工作原理如下:

  1. 默认情况下线程不会预创建,任务提交之后才会创建线程(不过设置 prestartAllCoreThreads 可以预创建核心线程)。
  2. 当核心线程满了之后不会新建线程,而是把任务堆积到工作队列中。
  3. 如果工作队列放不下了,然后才会新增线程,直至达到最大线程数。
  4. 如果工作队列满了,然后也已经达到最大线程数了,这时候来任务会执行拒绝策略。
  5. 如果线程空闲时间超过空闲存活时间,并且当前线程数大于核心线程数的则会销毁线程,直到线程数等于核心线程数(设置 allowCoreThreadTimeOut 为 true 可以回收核心线程,默认为 false)。

注意,核心线程和非核心线程在线程池中是一样的,并没有特殊的标识区分!

为什么默认情况下不会预创建线程

csharp 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        2, 5,
        60, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>()
);

// 默认情况:还没有线程
System.out.println("Pool size before: " + executor.getPoolSize());

// 预启动一个核心线程
executor.prestartCoreThread();
System.out.println("Pool size after prestartCoreThread: " + executor.getPoolSize());

// 预启动所有核心线程
executor.prestartAllCoreThreads();
System.out.println("Pool size after prestartAllCoreThreads: " + executor.getPoolSize());

此时线程池里 一开始是没有任何线程的

  • 第一次提交任务时,才会创建一个线程来执行任务。
  • 如果有空闲线程在工作队列里,新的任务会被直接放到队列中,不会马上创建新线程。
  • 如果队列满了,才会继续创建新线程,直到达到 maximumPoolSize

也就是说:线程池是懒惰的,不浪费资源,只有来了任务才会创建线程。

prestartAllCoreThreads()

如果手动调用这个方法,那么就会预创建核心线程

Java 并发库中提供了哪些线程池实现?

Java 并发库中提供了 5 种常见的线程池实现,主要通过 Executors 工具类来创建。

一般来说。不建议使用Executors去创建线程池 ,而是通过ThreadPoolExectors去创建

1)FixedThreadPool:创建一个固定数量的线程池。

线程池中的线程数是固定的,空闲的线程会被复用。如果所有线程都在忙,则新任务会放入队列中等待。

适合负载稳定的场景,任务数量确定且不需要动态调整线程数。

2)CachedThreadPool:一个可以根据需要创建新线程的线程池。

线程池的线程数量没有上限,空闲线程会在 60 秒后被回收,如果有新任务且没有可用线程,会创建新线程。

适合短期大量并发任务的场景,任务执行时间短且线程数需求变化较大。

3)SingleThreadExecutor:创建一个只有单个线程的线程池。

只有一个线程处理任务,任务会按照提交顺序依次执行。

适用于需要保证任务按顺序执行的场景,或者不需要并发处理任务的情况。

4)ScheduledThreadPool:支持定时任务和周期性任务的线程池。

可以定时或以固定频率执行任务,线程池大小可以由用户指定。

适用于需要周期性任务执行的场景,如定时任务调度器。

5)WorkStealingPool:基于任务窃取算法的线程池。

线程池中的每个线程维护一个双端队列(deque),线程可以从自己的队列中取任务执行。如果线程的任务队列为空,它可以从其他线程的队列中"窃取"任务来执行,达到负载均衡的效果。

适合大量小任务并行执行,特别是递归算法或大任务分解成小任务的场景。

不同线程池的选择总结:

  • FixedThreadPool 适合任务数量相对固定,且需要限制线程数的场景,避免线程过多占用系统资源。
  • CachedThreadPool 更适合大量短期任务或任务数量不确定的场景,能够根据任务量动态调整线程数。
  • SingleThreadExecutor 保证任务按顺序执行,适合要求严格顺序执行的场景。
  • ScheduledThreadPool 是定时任务的最佳选择,能够轻松实现周期性任务调度。
  • WorkStealingPool 适合处理大量的小任务,能更好地利用 CPU 资源。

Java 线程池有哪些拒绝策略?

一共提供了 4 种:

1)AbortPolicy,当任务队列满且没有线程空闲,此时添加任务会直接抛出 RejectedExecutionException 错误 ,这也是默认的拒绝策略。适用于必须通知调用者任务未能被执行的场景。

2)CallerRunsPolicy,当任务队列满且没有线程空闲,此时添加任务由即调用者线程执行。适用于希望通过减缓任务提交速度来稳定系统的场景。

3)DiscardOldestPolicy,当任务队列满且没有线程空闲,会删除最早的任务,然后重新提交当前任务。适用于希望丢弃最旧的任务以保证新的重要任务能够被处理的场景。

4)DiscardPolicy直接丢弃当前提交的任务,不会执行任何操作,也不会抛出异常。适用于对部分任务丢弃没有影响的场景,或系统负载较高时不需要处理所有任务。

如何设置最大线程池?

线程池的线程数设置需要看具体执行的任务是什么类型的。

任务类型可以分:CPU 密集型任务和 I/O 密集型任务。

Runtime.getRuntime().availableProcessors()这个可以查看电脑的CPU核心数

CPU 密集型任务

CPU 密集型任务,就好比单纯的数学计算任务,它不会涉及 I/O 操作,也就是说它可以充分利用 CPU 资源(如果涉及 I/O,在进行 I/O 的时候 CPU 是空闲的),不会因为 I/O 操作被阻塞,因此不需要很多线程,线程多了上下文开销反而会变多。

根据经验法则,CPU 密集型任务线程数 = CPU 核心数 + 1

I/O 密集型任务

I/O 密集型任务,有很多 I/O 操作,例如文件的读取、数据库的读取等等,任务在读取这些数据的时候,是无法利用 CPU 的,对应的线程会被阻塞等待 I/O 读取完成,因此如果任务比较多,就需要有更多的线程来执行任务,来提高等待 I/O 时候的 CPU 利用率。

根据经验法则,I/O 密集型任务线程数 = CPU 核心数 * 2 或更多一些。

ini 复制代码
// 获取 CPU 核心数
int cpuCount = Runtime.getRuntime().availableProcessors();

// CPU 密集型线程池
ExecutorService cpuPool = Executors.newFixedThreadPool(cpuCount + 1);

// I/O 密集型线程池(经验值:核心数 * 2)
ExecutorService ioPool = Executors.newFixedThreadPool(cpuCount * 2);

>以上公式仅是一个纯理论值,仅供参考!在生产上,需要考虑机器的硬件配置,设置预期的 CPU 利用率、CPU负载等因素,再通过实际的测试不断调整得到合理的线程池配置参数。

相关推荐
对不起初见2 小时前
PlantUML 完整教程:从入门到精通
前端·后端
你的人类朋友2 小时前
HTTP请求结合HMAC增加安全性
前端·后端·安全
武子康2 小时前
大数据-113 Flink 源算子详解:非并行源(Non-Parallel Source)的原理与应用场景
大数据·后端·flink
QZQ541882 小时前
高性能现代CPP--表达式模板(expression templates)
后端
莹Innsane3 小时前
使用 VictoriaLogs 存储和查询服务器日志
后端
karry_k3 小时前
BlockingQueue与SynchronousQueue
后端
前端伪大叔3 小时前
第15篇:Freqtrade策略不跑、跑错、跑飞?那可能是这几个参数没配好
前端·javascript·后端
Postkarte不想说话3 小时前
使用MSF生成反弹shell
后端
golang学习记3 小时前
Go 项目目录结构最佳实践:少即是多,实用至上
后端