掘金 后端 ( ) • 2024-07-02 20:35

highlight: gruvbox-dark theme: cyanosis

你好,我是 shengjk1,多年大厂经验,努力构建 通俗易懂的、好玩的编程语言教程。 欢迎关注!你会有如下收益:

  1. 了解大厂经验
  2. 拥有和大厂相匹配的技术等

希望看什么,评论或者私信告诉我!

一、前言

上一章节我们详解介绍了线程池的一些关键性知识。这章我们会详细介绍各个线程池的原理

二、线程池原理

2.1 ThreadPoolExecutor 原理

图片.png

从图中可以看出,当提交一个新任务到线程池时,线程池的处理流程如下。

  1. 线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
  2. 线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
  3. 线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务

2.2 ThreadPoolExecutor的 execute 方法原理

图片.png

ThreadPoolExecutor执行execute方法分下面4种情况。

  1. 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
  2. 如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
  3. 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。
  4. 如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁。

2.3 FixedThreadPool

2.3.1 FixedThreadPool 简介

FixedThreadPool被称为可重用固定线程数的线程池,通过上一篇我们知道它是通过 ThreadPoolExecutor 来实现的,下面是FixedThreadPool的源代码实现。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

FixedThreadPool的corePoolSize和maximumPoolSize都被设置为创建FixedThreadPool时指定的参数nThreads。

当线程池中的线程数大于corePoolSize时,keepAliveTime为多余的空闲线程等待新任务的最长时间,超过这个时间后多余的线程将被终止。这里把keepAliveTime设置为0L,意味着多余的空闲线程会被立即终止

2.3.2 FixedThreadPool 的 execute 方法原理

图片.png

  1. 如果当前运行的线程数少于corePoolSize,则创建新线程来执行任务。
  2. 在线程池完成预热之后(当前运行的线程数等于corePoolSize),将任务加入LinkedBlockingQueue。
  3. 线程执行完1中的任务后,会在循环中反复从LinkedBlockingQueue获取任务来执行。FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。

使用无界队列作为工作队列会对线程池带来如下影响。

  1. 当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize。
  2. 由于1,使用无界队列时maximumPoolSize将是一个无效参数。
  3. 由于1和2,使用无界队列时keepAliveTime将是一个无效参数。
  4. 由于使用无界队列,运行中的FixedThreadPool(未执行方法shutdown()或shutdownNow())不会拒绝任务(不会调用RejectedExecutionHandler.rejectedExecution方法)

2.3.2 FixedThreadPool 优缺点及应用场景

FixedThreadPool 是一个固定大小的线程池,在实际应用中有一些适合的场景和优点:

适用场景:

  1. 资源管理:FixedThreadPool 适合于资源受限的情况,因为它可以限制并发线程的数量,避免资源耗尽或者系统负载过高。
  2. 任务一致性:适合执行一组数量固定的相同类型或相似类型的任务,这样可以保持线程池中的线程数量恒定。
  3. 控制并发度:适合于需要限制并发度的场景,比如执行文件处理、网络请求或数据库操作,以避免并发线程过多而导致性能下降。
  4. 简单任务:适合于执行较为简单的短时任务,因为固定大小的线程池可以降低线程的创建和销毁开销。
  5. 避免资源竞争:适合于避免因为线程数量过多导致资源竞争和线程调度开销较大的情况。

优点:

  1. 固定线程数:FixedThreadPool 的线程数量固定,可以更好地控制系统的并发度,避免过多的线程导致资源消耗和线程调度开销。
  2. 资源管理:对于有限的资源环境,在 ThreadPool 中固定线程数可以更好地管理资源,避免资源耗尽问题。
  3. 优化性能:固定大小的线程池可以避免线程创建和销毁的开销,对于执行频繁的任务能够提升性能。
  4. 任务排队:当任务数量大于线程池大小时,FixedThreadPool 中的任务会被放入队列中等待执行,保证任务不会丢失。

示例场景:

  • Web 服务器中处理请求:对于固定数量的请求处理器线程池,能够控制同时处理请求数量,避免处理请求的过度延迟。
  • 数据库查询:限制并发的数据库查询线程数量,避免数据库连接过度消耗。
  • 后台任务处理:用于处理一些耗时任务,比如定时检查或清理任务。
  • 池化资源管理:对于有限数量的资源如连接池、线程池等管理。

FixedThreadPool 的固定线程数量能够让你更好地控制系统资源的分配和线程的使用,适用于一些需求稳定并且相对简单的线程管理场景。

三、总结

文章深入解析了Java中线程池的核心工作机制,重点阐释了ThreadPoolExecutor如何管理线程和任务,以及FixedThreadPool在实际应用中的适用场景、优点和局限性。通过具体源代码和执行流程分析,让读者对线程池的实现原理有了清晰的认识。