线程池中的线程增长与回收策略

学习自:漫画:聊聊线程池中,线程的增长/回收策略
今天在微信推送时看到这么一篇文章,刚好回顾一下之前学习的线程池知识。

线程池

为了避免系统频繁的创建和销毁线程,我们可以将创建的线程进行复用。创建线程变成了从线程池获取空闲的线程,关闭线程变成了向池子中归还线程。

线程池的好处
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗

第二:提高响应速度。当任务完成时,任务可以不需要等到线程创建就能立即执行。

第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。

JDK对线程池的支持在Java1.5中提供了Executor,可以让我们有效的管理和控制我们的线程,其本质也是线程池。

Java里面线程池的顶级接口是Executor,不过真正的线程池接口是ExecutorServiceExecutorService 的默认实现是 ThreadPoolExecutor;普通类 Executors 里面调用的就是 ThreadPoolExecutor

线程池策略

线程池中的参数意义

这里我们来重新认知一下线程池吧!

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

corePoolSize:表示核心线程数量
maximumPoolSize:表示最大线程数量
keepAliveTime:表示核心线程数之外的线程,最大空闲存活的时长;
unit:表示keepAliveTime 的时间单位;
workQueue:表示线程池的任务等待队列;
threadFactory:线程工厂,用来为线程池创建线程;可以用于给线程设置名字,一般默认不设置参数

handler:饱和策略,当线程池无法处理任务时的拒绝方式;这是当任务对列和线程池都满时,采取的应对策略,默认是AbordPolicy,表示无法处理新任务。并且抛出异常(RejectedExecutionException)。

  1. CallerRunsPolicy:用调用者所在的线程来处理任务。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
  2. DiscardPoliicy:不能执行的任务,并将该任务删除。
  3. DiscardOldestPolicy:丢弃队列最近的任务,并执行当前任务。

其中这里面的参数是互相影响的,例如workQueue配置不正确,会导致maximumPoolSize形同虚设,导致线程池中的线程,永远无法增长到核心线程数maximumPoolSize配置的线程数。

线程池中的线程增长策略

线程池中的增长策略主要这三个参数有关。
corePoolSize:核心线程数量
maximumPoolSize:最大线程数量
workQueue:线程池的任务等待队列

在这里插入图片描述

线程池的任务处理策略:

如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;

如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;

如果线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

这里可以参考流程图,消化上面文字
在这里插入图片描述
结合上图,可以看出,我们执行ThreadPoolExecutor开始提交任务(execute)时,遇见的各种情况

如果线程数没有带到核心线程数(corePoolSize),则创建核心线程数处理任务,

如果线程数大于或者等于核心线程数(corePoolSize),则将任务加入任务队列(workQueue),线程池中的空闲线程会不断的从任务队列中取出任务进行处理.

如果任务队列满了,且线程数没有达到最大线程数(maximumPoolSize),则会创建非核心线程去处理任务.

如果线程数超过了最大线程数,则执行饱和策略

测试成果

我们来看一下这么一个线程池的创建代码吧!

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

我们可以看出,设置了核心线程为30个,最大线程数为60,60L, TimeUnit.MILLISECONDS,是指核心线程数之外的线程,最大空闲存活的时长为60毫秒,其线程池的任务等待队列为LinkedBlockingQueue<Runnable>

该线程池的任务等待队列为LinkedBlockingQueue<Runnable>,我们来看一下LinkedBlockingQueue<Runnable>的源码。


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);
}

从中可以看出,我们的LinkedBlockingQueue是如果不传递参数的话,默认是创建一个无界队列。

而从我们的例子上看来,线程池的任务等待队列是一个无界队列,那就表明,我们的队列不会满,除非将系统内存就消耗殆尽。

线程池中线程的收缩策略

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

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

corePoolSize:核心线程数;
maximumPoolSize:线程池的最大线程数;
keepAliveTime:核心线程数之外的线程,空闲存活的时长;
unitkeepAliveTime 的时间单位;
corePoolSizemaximumPoolSize 我们比较熟悉了,另外能够控制它的就是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。

查缺补漏

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

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

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

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

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

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

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

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

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

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

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

猜你喜欢

转载自blog.csdn.net/weixin_44710164/article/details/104684307