漫画:聊聊线程池中,线程的增长/回收策略

一、序

public static ExecutorService newThreadPool() {
  return new ThreadPoolExecutor(
    30, 60,
    60L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>());
}

我们今天就来借这个问题,聊聊线程池中维护的线程,它增长和回收的策略是什么样的?

二、线程池的策略

2.1 线程池的各项参数

当我们聊到线程池中线程的增长策略的时候,最抓眼球的就是它的核心线程数(corePoolSize)和最大线程数(maximumPoolSize),但是仅看这两个参数是不够全面的,线程数量的增长,还与任务等待队列有关系。

我们先来看看 ThreadPoolExecutor 最全参数的构造方法:

public ThreadPoolExecutor(
  int corePoolSize,
  int maximumPoolSize,
  long keepAliveTime,
  TimeUnit unit,
  BlockingQueue<Runnable> workQueue,
  ThreadFactory threadFactory,
  RejectedExecutionHandler handler) {
  // ...
}

简单解释一下各个参数是什么意思:

  • corePoolSize:核心线程数;

  • maximumPoolSize:线程池的最大线程数;

  • keepAliveTime:核心线程数之外的线程,最大空闲存活的时长;

  • unit:keepAliveTime 的时间单位;

  • workQueue:线程池的任务等待队列;

  • threadFractory:线程工厂,用来为线程池创建线程;

  • handler:拒绝策略,当线程池无法处理任务时的拒绝方式;

这其中很多参数的配置,都是相互影响的。例如任务等待队列 workQueue 配置不当,可能导致线程池中的线程,永远无法增长到核心线程数(maximumPoolSize)配置的线程数。

2.2 线程池中线程的增长策略

看到这里你应该就清楚了,线程池线程的增长策略,和 3 个参数有关系:

  • corePoolSize:核心线程数

  • maximumPoolSize:最大线程数;

  • workQueue:等待任务队列;

它们之前的关系是这样的:

接下来我们看看理想情况下,线程池中线程的增长策略。

默认情况下,初始时线程池是空的,当有新任务来了时,线程池开始通过线程工厂(threadFractory)创建线程来处理任务。

新的任务会不断的触发线程池中线程的创建,直到线程数量达到核心线程数(corePoolSize),接下来会停止线程的创建,而是将这个新任务放入任务等待队列(workQueue)。

新任务不断进入任务等待队列,当该队列满了时,开始重新创建线程处理任务,直到线程池中线程的数量,到达 maximumPoolSize 配置的数量。

到这一步时,线程池的线程数达到最大值,并且没有空闲的线程,任务队列也存满了任务,这时如果还有新的任务进来,就会触发线程池的拒绝策略(handler),如果没有配置拒绝策略就会抛出 RejectedExecutionException 异常。

到这里线程的增长策略就说清楚了,我们可以通过下图来了解完整的流程。

其中比较关键的就是任务的等待队列,无论等待队列的实现结构是什么样的,只有在它满的时候,线程池中的线程才会向最大线程数增长。但是一个能够满的队列,它的前提是必须是一个有界队列

这就是文章开头举的例子暗藏的坑,我们回顾一下前面构造的线程池。

public static ExecutorService newThreadPool() {
  return new ThreadPoolExecutor(
    30, 60,
    60L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>());
}

可以看到,这里虽然最大线程数是大于核心线程数的,但是它的等待队列配置的是一个 LinkedBlockingQueue,从名字上可以看出这是一个基于链表实现的阻塞队列,而用它的默认构造方法构造时,其容量设定为 Integer.MAX_VALUE,可以简单理解它是一个无界队列。

public LinkedBlockingQueue() {
  this(Integer.MAX_VALUE);
}

public LinkedBlockingQueue(int capacity) {
  if (capacity <= 0) throw new IllegalArgumentException();
  this.capacity = capacity;
  last = head = new Node<E>(null);
}

这也就是为什么说,这样构造的线程池,核心线程数的配置参数,永远都用不到,因为它的等待队列永远没有满的时候。

2.3 线程池中线程的收缩策略

线程池中执行的任务,总有执行结束的时候。那么线程池当线程池中存在大量空闲线程时,也会有一定的收缩策略,来回收线程池中多余的线程。

线程池中线程的收缩策略,和以下几个参数相关:

  • corePoolSize:核心线程数;

  • maximumPoolSize:线程池的最大线程数;

  • keepAliveTime:核心线程数之外的线程,空闲存活的时长;

  • unit:keepAliveTime 的时间单位;

corePoolSize 和 maximumPoolSize 我们比较熟悉了,另外能够控制它的就是 keepAliveTime 空闲存活时长,以及这个时长的单位。

当线程池中的线程数,超过核心线程数时。此时如果任务量下降,肯定会出现有一些线程处于无任务执行的空闲状态。那么如果这个线程的空闲时间超过了 keepAliveTime&unit 配置的时长后,就会被回收。

需要注意的是,对于线程池来说,它只负责管理线程,对于创建的线程是不区分所谓的「核心线程」和「非核心线程」的,它只对线程池中的线程总数进行管理,当回收的线程数达到 corePoolSize 时,回收的过程就会停止。

对于线程池的核心线程数中的线程,也有回收的办法,可以通过 allowCoreThreadTimeOut(true) 方法设置,在核心线程空闲的时候,一旦超过 keepAliveTime&unit 配置的时间,也将其回收掉。

public void allowCoreThreadTimeOut(boolean value) {
  if (value && keepAliveTime <= 0)
    throw new IllegalArgumentException("Core threads must have nonzero keep alive times");
  if (value != allowCoreThreadTimeOut) {
    allowCoreThreadTimeOut = value;
    if (value)
      interruptIdleWorkers();
  }
}

allowCoreThreadTimeOut() 能被设置的前提是 keepAliveTime 不能为 0。

2.3 查缺补漏

1. 等待队列还会影响拒绝策略

等待队列如果配置成了无界队列,不光影响线程数量从核心线程数向最大线程数的增长,还会导致配置的拒绝策略永远得不到执行。

因为只有在线程池中的工作线程数量已经达到核心线程数,并且此时等待队列也满了的情况下,拒绝策略才能生效。

2. 核心线程数可以被「预热」

前面提到默认的情况下,线程池中的线程是根据任务来增长的。但如果有需要,我们也可以提前准备好线程池的核心线程,来应对突然的高并发任务,例如在抢购系统中就经常有这样的需要。

此时就可以利用 prestartCoreThread() 或者 prestartAllCoreThreads() 来提前创建核心线程,这种方式被我们称为「预热」。

3.  对于需要无界队列的场景,怎么办?

需求是多变的,我们肯定会碰到需要使用无界队列的场景,那么这种场景下配置的 maximumPoolSize 就是无效的。

此时就可以参考 Executors 中 newFixedThreadPool() 创建线程池的过程,将 corePoolSize 和 maximumPoolSize 保持一致即可。

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

此时核心线程数就是最大线程数,只有增长到这个数量才会将任务放入等待队列,来保证我们配置的线程数量都得到了使用。

4. 线程池是公平的吗?

所谓的公平,就是先到的任务会被先执行。这在线程池中,显然是不公平的。

不提线程池中线程执行任务是通过系统去调度的,这一点就决定了任务的执行顺序是无法保证的,这就是是非公平的。另外只从线程池本身的角度来看,我们只看提交的任务顺序来看,它也是非公平的。

首先前面到的任务,如果线程池的核心线程已经分配出去了,此时这个任务就会进入任务队列,那么如果任务队列满了之后,新到的任务会直接由线程池新创建线程去处理,直到线程数达到最大线程数。

那么此时,任务队列中的任务,虽然先添加进线程池等待处理,但是这些任务的处理时机,是晚于后续新创建线程去处理的任务的,所以说仅从任务的角度,依然是非公平的。

三、小结时刻

本文我们聊到了线程池中,对于线程数量的增长和收缩策略。

在这里我们简单总结一下:

1. 增长策略。默认情况下,线程池是根据任务先创建足够核心线程数的线程去执行任务,当核心线程满了时将任务放入等待队列。待队列满了的时候,继续创建新线程执行任务直到到达最大线程数停止。再有新任务的话,那就只能执行拒绝策略或是抛出异常。

2. 收缩策略。当线程池线程数量大于核心线程数 && 当前有空闲线程 && 空闲线程的空闲时间大于 keepAliveTime 时,会对该空闲线程进行回收,直到线程数量等于核心线程数为止。

总之要谨记,慎用无界队列。

最后,本文对你有帮助吗?留言、转发、点好看是最大的支持,谢谢!


公众号后台回复成长『成长』,将会得到我准备的学习资料。

推荐阅读:
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 要获取自定义线程池线程,可以使用`ThreadPoolExecutor`类的`getActiveCount()`方法来获取当前线程池正在执行任务的线程数量,再使用`ThreadPoolExecutor`类的`getPoolSize()`方法来获取当前线程池线程总数。代码示例如下: ```java import java.util.concurrent.*; public class CustomThreadPool { public static void main(String[] args) { // 创建自定义线程池 ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, 4, 100, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(10) ); // 提交任务 for (int i = 0; i < 6; i++) { executor.execute(new Task(i)); } // 获取线程池正在执行任务的线程数量 int activeCount = executor.getActiveCount(); System.out.println("当前线程池正在执行任务的线程数量:" + activeCount); // 获取线程池线程总数 int poolSize = executor.getPoolSize(); System.out.println("当前线程池线程总数:" + poolSize); // 关闭线程池 executor.shutdown(); } } class Task implements Runnable { private int taskId; public Task(int taskId) { this.taskId = taskId; } @Override public void run() { System.out.println("任务 " + taskId + " 正在执行..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("任务 " + taskId + " 执行完成!"); } } ``` 上述代码,`CustomThreadPool`类创建了一个自定义线程池`executor`,并提交了6个任务。然后使用`executor.getActiveCount()`方法和`executor.getPoolSize()`方法获取当前线程池线程数量信息,并打印输出。最后关闭线程池。 ### 回答2: 在Java,我们可以通过以下步骤来获取自定义线程池线程: 1. 首先,我们需要创建一个自定义的线程池对象。可以使用ThreadPoolExecutor类来实现一个自定义线程池,该类提供了许多可调整的参数,例如核心线程数、最大线程数、闲置线程存活时间等等。 2. 在创建自定义线程池对象之后,我们可以通过调用execute()方法将任务提交到线程池。execute()方法接受一个Runnable对象作为参数,该对象代表一个待执行的任务。 3. 如果我们想获取线程池线程,可以调用线程池对象的getPoolSize()方法,该方法返回当前线程池线程数量。这可以帮助我们了解线程池的使用情况。 4. 另外,如果我们想获取线程池的每个线程的详细信息,可以通过调用线程池对象的getActiveThreads()方法来获取活动线程的数组。然后,我们可以遍历该数组以获取每个线程的相关信息,例如线程的ID、名称等等。 总结起来,要获取自定义线程池线程,我们需要创建一个自定义线程池对象,然后通过调用相应的方法来获取线程池线程的数量或者每个线程的详细信息。这样,我们可以更好地了解线程池的使用情况,并且对线程池的调度和管理进行更精确的控制。 ### 回答3: Java获取自定义线程池线程可以通过ThreadPoolExecutor类的getActiveCount()和getPoolSize()方法来实现。 首先,我们需要先创建一个自定义的线程池对象,例如: ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); 其corePoolSize为线程池核心线程的数量,maximumPoolSize为线程池允许的最大线程数量,keepAliveTime为线程空闲时的存活时间,unit为存活时间的单位,workQueue为任务队列,用于存放待执行的任务。 接下来,我们使用executor对象可以调用getActiveCount()方法来获取当前活动的线程数量,即正在执行任务的线程数量。示例代码如下: int activeThreadCount = executor.getActiveCount(); 同时,我们还可以使用getPoolSize()方法来获取当前线程池线程数量,包括核心线程和非核心线程。示例代码如下: int threadPoolSize = executor.getPoolSize(); 这样,我们就可以通过上述方法获得自定义线程池线程的数量了。 需要注意的是,使用线程池时要注意及时关闭线程池,以免引发线程泄漏或资源浪费的问题。可以通过调用executor.shutdown()方法来关闭线程池。 总之,我们可以通过ThreadPoolExecutor类的getActiveCount()和getPoolSize()方法来获取自定义线程池线程的数量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值