Java线程池ThreadPoolExecutor(二)

156 阅读6分钟

《Java线程池ThreadPoolExecutor(一)》中,介绍了ThreadPoolExecutor的一些关键属性、工作线程执行流程。现在,一起来看看Java线程池的创建、关闭、调优和监控。

1 创建线程池

1.1 使用构造器

使用ThreadPoolExecutor构造器来创建线程池,其他参数在《Java线程池ThreadPoolExecutor(一)》中过介绍。现在来看看workQueue(任务队列)如何设置。

workQueue是BlockingQueue<Runnable>的子类,有以下可选项:

  • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
  • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。
  • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

1.2 使用Executors工厂类

Executors类中提供了很多工厂方法,可以创建几种类型的ThreadPoolExecutor。

FixedThreadPool

固定线程数的线程池,corePoolSize和maximumPoolSize设置为相同的值,即全是核心线程,无法创建应急线程,线程池中线程也不会因空闲而终止。适用于任务量已知,相对耗时的任务。

image.png 使用无界队列LinkedBlockingQueue作为工作队列(容量为Integer.MAX_VALUE),有如下影响:

  1. 当线程池中的线程数达到corePoolSize后,新任务都将放入无界队列中等待;
  2. maximumPoolSize、keepAliveTime将是无效参数。
  3. 运行中的FixedThreadPool不会拒绝任何任务,因此较难感知到任务是否积压。

SingleThreadExecutor

corePoolSize和maximumPoolSize被设置为1,其他参数与FixedThreadPool相同。 image.png

CachedThreadPool

这是一个会根据需要动态创建新线程的线程池。

  • corePoolSize设置为0,maximumPoolSize被设置为Integer.MAX_VALUE,相当于可以不受限制的创建应急线程。
  • keepAliveTime设置为60L,空闲线程超过60秒后将会被终止。
  • 使用没有容量的SynchronousQueue作为线程池的工作队列。如果提交任务的速度高于已有线程处理速度时,CachedThreadPool会不断创建新线程。极端情况下,会因为创建过多线程而耗尽CPU和内存资源。

CachedThreadPool适合任务数比较密集,但每个任务执行时间较短的情况。 image.png

ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor继承自ThreadPoolExecutor。它主要用来在给定的延迟之后运行任务,或者定期执行任务。功能与Timer类似,但它的功能更强大、更灵活。 image.png DelayQueue是一个无界队列,因此maximumPoolSize失去意义。

当调用scheduleAtFixedRate()方法或者scheduleWithFixedDelay()方法时,会向DelayQueue添加一个实现了RunnableScheduledFutur接口的ScheduledFutureTask。 ScheduledFutureTask主要包含3个成员变量:

  • long time,表示这个任务将要被执行的具体时间。
  • long sequenceNumber,表示这个任务被添加到ScheduledThreadPoolExecutor中的序号。
  • long period,表示任务执行的间隔周期。

image.png DelayQueue封装了一个PriorityQueue,会对队列中的ScheduledFutureTask进行排序,time小的排在前面,time相同时sequenceNumber小的排在前面(先提交的任务将被先执行)。 image.png

当某个线程执行一个到期任务时,会将任务的time更新为下次执行时间,并重新放回队列中排序。 image.png

2 关闭线程池

ThreadPoolExecutor有两个方法可以关闭线程池:shutdown和shutdownNow。原理都是遍历所有工作线程,逐个调用Thread.interrupt()来中断线程。

共同点:当调用以上任意一个方法后,ThreadPoolExecutor.isShutdown会返回true,且线程池将拒绝提交新的任务。 image.png 它俩的区别在于:

  • shutdown设置线程池状态为SHUTDOWN,只会interruptIdleWorkers,即立即中断等待任务的线程,而对正在执行任务的线程不会中断。即执行中的任务会继续执行,任务队列中剩余任务也会被执行。 image.png image.png
  • shutdownNow设置线程池状态为STOP,会调用interruptIfStarted,即中断所有已经启动的线程,即使线程正在某个执行,并返回任务队列中的剩余任务。 image.png

该使用哪一个方法呢?这取决于任务特性,通常使用shutdown;如果任务不一定要执行完,则可以调用shutdownNow。

注意:如果任务无法响应,线程可能不会如期终止。如下面的代码:由于runnable1无法响应interrupt,导致线程池始终有活跃线程。

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Slf4j(topic = "c.ShutdownThread")
public class ShutdownThread {
  public static final ThreadPoolExecutor EXECUTOR_SERVICE = (ThreadPoolExecutor) Executors.newFixedThreadPool(3);

  public static void main(String[] args) throws InterruptedException {
    // 启动3个基本线程
    EXECUTOR_SERVICE.prestartAllCoreThreads();
    Runnable runnable1 = () -> {
      //用死循环模拟没有超时时间的 RPC调用
      while (true) {
      }
    };
    
    Runnable runnable2 = () -> {
      try {
        TimeUnit.SECONDS.sleep(15);
      } catch (InterruptedException e) {
        log.info("runnable2 被中断");
        throw new RuntimeException(e);
      }
    };
    EXECUTOR_SERVICE.submit(runnable1);
    EXECUTOR_SERVICE.submit(runnable2);

    TimeUnit.SECONDS.sleep(3);
    EXECUTOR_SERVICE.shutdownNow();
    TimeUnit.SECONDS.sleep(3);
    log.info("活跃线程数" + EXECUTOR_SERVICE.getActiveCount());
    if (EXECUTOR_SERVICE.isTerminated()) {
      log.info("线程池终止");
    }
  }
}

image.png

3 合理配置线程池

想要合理配置线程池,得先分析任务特性:

  • 任务性质:CPU密集型任务、IO密集型任务和混合型任务
  • 任务优先级:高、中和低。
  • 任务执行时间:长、中和短。
  • 任务依赖性:是否依赖其他系统资源,如数据库连接、网络连接等。 有以下建议:
  1. 性质不同的任务,可以分开使用不同的线程池。
  2. CPU密集型任务应配置尽可能小的线程,如配置CPU数+1个线程的线程池;
  3. IO密集型任务,线程并不是一直在占用CPU,应配置尽可能多的线程,如2倍cpu数;
  4. 优先级不同的任务,可以使用PriorityBlockingQueue,它可以让优先级高的任务先执行。
  5. 依赖于其他系统资源的任务,往往CPU空闲时间越长,可以设置更大的线程数。
  6. 建议使用有界队列,且拒绝策略能够给出提示或告警,这将增加线程池的稳定性和预警能力。

4 线程池监控

监控线程池,方便在出现问题时能够快速定位和处理。可调用ThreadPoolExecutor的这些方法:

  • getTaskCount:已提交任务的大致总数;
  • getCompletedTaskCount:线程池已完成的任务数量,是个近似值。
  • getLargestPoolSize:线程池曾经创建过的最大线程数量,判断线程池是否曾经满过。
  • getPoolSize:线程池的线程数量,即ThreadPoolExecutor.workers.size();
  • getActiveCount:获取活动的线程数。 此外,通过继承ThreadPoolExecutor来自定义线程池,重写beforeExecute、afterExecute和terminated方法,在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。 image.png