【Java】线程池

340 阅读7分钟

由于多核 CPU 成为主流,并行和并发成为程序员开发的主要目标,线程池是 java 中常用的并发工具,具有简单、易用、高效的特点。

图源:Java线程池实现原理及其在美团业务中的实践 - 美团技术团队 (meituan.com)

线程池的作用

线程池的作用主要是以下四个方面:

  1. 降低消耗,提高吞吐量。通过重用已存在的线程,降低线程创建和销毁造成的消耗;
  2. 提高响应速度。当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
  3. 限制线程数量。对线程的并发数进行管控,防止无限制地创建线程而导致 OOM 或 CPU 过度切换。
  4. 提供增强功能。比如定时执行,延迟线程池。

线程池的设计

线程池设计的主要目的是提高线程的利用率和防止无限制的创建线程。本质上是一种将资源同一管理的思想,类似的还有我们的数据库连接池等。

在 Java 中,线程池主要是 ThreadPoolExecutor 类型,它的实现是一个生产者/消费者的设计模式。通过一个任务队列对线程和任务进行解耦。但任务的创建也并不是完全于线程没有直接联系,某些情况下会跳过任务队列直接创建线程。

主要参数

ThreadPoolExecutor 类关键构造函数为:

public ThreadPoolExecutor(int corePoolSize,                  // 核心线程数
                          int maximumPoolSize,               // 最大线程数
                          long keepAliveTime,                // 保活时间
                          TimeUnit unit,                     // 时间单位
                          BlockingQueue<Runnable> workQueue, // 任务队列
                          ThreadFactory threadFactory,       // 线程工厂
                          RejectedExecutionHandler handler   // 拒绝处理器
) {}
  • 核心线程数corePoolSize
    当线程池中线程为空闲状态时,会回收多余的线程,保持可用线程数最少为核心线程数。

  • 最大线程数maximunPoolSize
    当线程数量达到最大线程数时,线程池不会再创建任何线程,而是将任务放在任务队列中等待执行。

  • 保活时间 :keepAliveTime
    线程空闲时,超过保活时间的多余线程就会被回收。

  • 时间单位 :TimeUnit unit
    保活时间的时间单位。

  • 任务队列 :workQueue
    如果目前没有空闲线程,则会将任务放在任务队列中等待空闲线程执行。

  • 线程工厂 :threadFactory
    用于创建线程,对线程做处理,默认的线程工厂仅对线程名字进行定制。

  • 拒绝处理器RejectedExecutionHandler
    也称为拒绝策略,如果无法创建线程并且阻塞队列已满,任务会被拒绝执行,此时调用拒绝执行处理器进行处理。

工作流程

线程工作流程

  1. executesubmit 传入一个任务时,唤醒一个休眠的线程进行执行。

  2. 如果没有休眠的线程,则判断是否达到最大线程数,如果没有则创建一个线程执行任务。

  3. 当线程执行完毕之后,会去队列中获取新的任务运行。

  4. 如果队列中没有任务,则线程会进行睡眠,睡眠时间就是保活时间。

  5. 线程休眠结束后,会尝试从队列中再次获取任务,如果队列是空的,则判断当前线程数是否大于核心线程数,如果大于则线程销毁;如果小于则继续休眠。

任务处理流程

  1. 如果当前线程数小于核心线程数,立即创建线程执行任务。否则尝试投递到任务队列中。

  2. 如果任务队列可以投递,就投递到任务队列中等待执行。如果任务队列已满,则尝试创建新线程。

  3. 如果当前线程数未达到最大线程数,就创建新线程进行执行。否则执行拒绝策略。

生命周期

ThreadPoolExecutor 的生命周期中共有五种状态,分别为:

  • RUNNING 状态,线程池正常执行。
  • SHTUDOWN 状态,线程池正常执行队列中的任务,但不接受新的任务。
  • STOP 状态,线程池暂停所有任务的执行,且不接受新的任务。
  • TIDYING 状态,工作线程数为 0。
  • TERMINATED 状态,线程池终止。

image.png

线程池类型

Executors 类帮我们定义好了多种类型的线程池创建方法,我们只需要调用它的静态方法就能创建各种类型的线程池。这些线程池类型实际大部分上是帮我们预定好参数的 ThreadPoolExecutor 对象。

可缓冲线程池

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, 
                                  Integer.MAX_VALUE,
                                  60L, 
                                  TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>()); // 同步队列
}

可缓冲线程池没有核心线程数,默认最大线程数为 Integer.MAX_VALUE,超时时间默认为 60 秒。阻塞队列采用同步队列实现,每次 offer 失败则表示没有空闲线程,此时线程池会立刻创建线程进行消费。

可缓冲线程池的弊端在于,最大线程数可以理解为无上限,只要任务队列满了就会不断地创建线程,因此会带来很多创建和销毁线程的消耗,甚至可能会出现无节制创建线程的情况,

定长线程池

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, 
                                  nThreads,
                                  0L, 
                                  TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(), // 无界队列
                                  threadFactory);
}

定长线程池的核心线程数和最大线程数相等,因此没有多余线程,不需要超时时间。阻塞队列采用 LinkedBlockingQueue,没有长度限制。是标准的线程模型。

定长线程池的弊端在于,由于采用无界队列作为任务队列,因此可能会出现无节制地投递任务地情况,这时也可能导致 OOM。

单线程线程池

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 
                                1,
                                0L, 
                                TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>())); // 无界队列
}

直译是“单任务执行器”,核心线程数和最大线程数都等于 1,也没有超时时间,阻塞队列采用 LinkedBlockingQueue 实现,没有长度限制,设计的目的在于让任务顺序执行。

单线程线程池由于同样无界队列,比定长线程池更容易触发 OOM。

周期线程池

public class ScheduledThreadPoolExecutor
    extends ThreadPoolExecutor
    implements ScheduledExecutorService {
    
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, 
                new DelayedWorkQueue()); // 延迟队列
    }
}

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

ScheduledThreadPoolExecutorThreadPoolExecutor 的子类,其内部实现了延迟队列DelayedWorkQueue)用于让任务按时执行。 它可以指定核心线程数量,最大线程数为整型最大值,没有超时时间。

周期线程池的弊端是存在死锁的风险,如果上一周期的任务未执行完毕,则会创建新的线程来执行当前周期的任务,如果任务存在同步代码块,则有可能发生死锁。

此外还有一个单线程周期线程池,它与周期线程池的区别在于核心线程数固定为 1。

public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
    return new DelegatedScheduledExecutorService
        (new ScheduledThreadPoolExecutor(1));
}

抢占式线程池

public class ForkJoinPool extends AbstractExecutorService {
    public ForkJoinPool(int parallelism,                     // 并行度
                        ForkJoinWorkerThreadFactory factory, // 线程工厂
                        UncaughtExceptionHandler handler,    // 失败处理器
                        boolean asyncMode                    // 异步模式
                        ) {
        this(checkParallelism(parallelism),
             checkFactory(factory),
             handler,
             asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
             "ForkJoinPool-" + nextPoolId() + "-worker-");
        checkPermission();
    }
}

public static ExecutorService newWorkStealingPool(int parallelism) {
    return new ForkJoinPool
        (parallelism,
         ForkJoinPool.defaultForkJoinWorkerThreadFactory,
         null, true);
}

抢占式线程池使用 ForkJoinPool 而不是 ThreadPoolExecutor 实现。线程数为并行线程数,默认为操作系统核心数。任务提交给线程池时会公平地分发给每条线程,如果某条线程执行速度快,可以到其他线程的任务队列末端去抢占任务执行,因此叫抢占式线程池。

拒绝策略

线程池默认提供四种拒绝策略:

  • AbortPolicy:默认策略,丢弃任务并抛出 RejectedExecutionException

  • DiscardPolicy:丢弃任务,且不进行任何提醒。

  • DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交。

  • CallerRunsPolicy:让调用线程执行。