Java 中创建线程池的五种方法及比较

转(部分调整):JAVA 中创建线程池的五种方法及比较

在 Java 中创建线程池主要有两种方式:一种是通过Executors工厂类提供的方法,该类提供了四种不同类型的线程池供开发者快速使用;另一种是直接通过ThreadPoolExecutor类进行自定义创建。

一、通过Executors工厂类创建线程池

Executors类提供了一系列静态工厂方法,简化了常见线程池的创建过程。

1. newCachedThreadPool

创建一个可缓存的线程池。当任务量增加时,若现有线程不足,则会新建线程;当线程空闲一段时间后,若线程数超过处理所需,则会被回收。

13540619

代码示例:

private static void createCachedThreadPool() {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < 10; i++) {
        final int index = i;
        executorService.execute(() -> {
            // 获取线程名称,默认格式:pool-1-thread-1
            System.out.println(DateUtil.now() + " " + Thread.currentThread().getName() + " " + index);
            // 等待 2 秒
            sleep(2000);
        });
    }
}

执行效果:

newCachedThreadPool 示例

由于初始线程池中没有线程,且当线程不足时会不断新建线程,因此执行过程中会看到不同的线程名称。

2. newFixedThreadPool

创建一个固定大小的线程池,用于控制并发执行的线程数量。超出线程池容量的任务会在队列中等待执行。

代码示例:

private static void createFixedThreadPool() {
    ExecutorService executorService = Executors.newFixedThreadPool(3);
    for (int i = 0; i < 10; i++) {
        final int index = i;
        executorService.execute(() -> {
            // 获取线程名称,默认格式:pool-1-thread-1
            System.out.println(DateUtil.now() + " " + Thread.currentThread().getName() + " " + index);
            // 等待 2 秒
            sleep(2000);
        });
    }
}

执行效果:

newFixedThreadPool 示例

由于线程池大小固定为 3,因此只会看到 3 个不同的线程名称。当线程不足时,任务会进入队列等待线程空闲,导致日志输出间隔为 2 秒。

3. newScheduledThreadPool

创建一个支持定时及周期性执行任务的线程池。

代码示例:

private static void createScheduledThreadPool() {
    ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
    System.out.println(DateUtil.now() + " 提交任务");
    for (int i = 0; i < 10; i++) {
        final int index = i;
        executorService.schedule(() -> {
            // 获取线程名称,默认格式:pool-1-thread-1
            System.out.println(DateUtil.now() + " " + Thread.currentThread().getName() + " " + index);
            // 等待 2 秒
            sleep(2000);
        }, 3, TimeUnit.SECONDS);
    }
}

执行效果:

newScheduledThreadPool 示例

由于设置了 3 秒的延迟,任务会在提交 3 秒后开始执行。此外,由于核心线程数为 3,当线程不足时,任务会进入队列等待空闲线程,导致日志输出间隔为 2 秒。

注意:此处使用的是ScheduledExecutorService类的schedule()方法,而非ExecutorService类的execute()方法。

4. newSingleThreadExecutor

创建一个单线程的线程池,可以确保所有任务按照指定的顺序(如 FIFO、LIFO 或优先级)依次执行。

代码示例:

private static void createSingleThreadPool() {
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    for (int i = 0; i < 10; i++) {
        final int index = i;
        executorService.execute(() -> {
            // 获取线程名称,默认格式:pool-1-thread-1
            System.out.println(DateUtil.now() + " " + Thread.currentThread().getName() + " " + index);
            // 等待 2 秒
            sleep(2000);
        });
    }
}

执行效果:

newSingleThreadExecutor 示例

由于线程池中只有一个线程,所有任务都由该线程执行,因此线程名称相同,并且每隔 2 秒按顺序输出。

二、通过ThreadPoolExecutor自定义创建线程池

ThreadPoolExecutor类提供了多种构造方法,允许开发者根据具体需求自定义线程池。

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

1. 核心参数详解

ThreadPoolExecutor的构造方法包含以下七个核心参数:

  1. corePoolSize:核心线程数,线程池中始终保持存活的线程数量。

  2. maximumPoolSize:最大线程数,线程池允许创建的最大线程数量。

  3. keepAliveTime:线程空闲时间,当线程池中的线程数量超过corePoolSize时,这些多余的空闲线程在终止前会等待新任务的最长时间。

  4. unitkeepAliveTime参数的时间单位。

  5. workQueue:一个阻塞队列,用于存储等待执行的任务。所有队列均是线程安全的,有 7 种可选类型。

    参数 描述
    ArrayBlockingQueue 一个由数组结构组成的有界阻塞队列。
    LinkedBlockingQueue 一个由链表结构组成的有界(或无界)阻塞队列。
    SynchronousQueue 一个不存储元素的阻塞队列,即直接将任务提交给线程而不进行保持。
    PriorityBlockingQueue 一个支持优先级排序的无界阻塞队列。
    DelayQueue 一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
    LinkedTransferQueue 一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
    LinkedBlockingDeque 一个由链表结构组成的双向阻塞队列。

    其中,LinkedBlockingQueueSynchronousQueue较为常用。线程池的任务排队策略与所选的BlockingQueue类型密切相关。

  6. threadFactory:线程工厂,用于创建新线程。默认情况下,它创建的是具有正常优先级、非守护状态的线程。

  7. handler:拒绝策略,当线程池无法处理新提交的任务时所采取的策略。有 4 种可选策略,默认为ThreadPoolExecutor.AbortPolicy

    参数 描述
    AbortPolicy 直接拒绝任务并抛出RejectedExecutionException异常。
    CallerRunsPolicy 在调用execute()方法的线程中直接运行该任务。
    DiscardOldestPolicy 抛弃任务队列中最旧(头部)的一个任务,并尝试重新提交当前任务。
    DiscardPolicy 直接抛弃当前任务,不作任何处理。

2. 线程池执行规则

ThreadPoolExecutor的执行规则如下:

  1. 当线程池中的线程数量小于corePoolSize时,即使有空闲线程,也会创建新线程来执行任务。
  2. 当线程池中的线程数量达到corePoolSize,且任务队列未满时,新提交的任务会被放入任务队列等待。
  3. 当线程池中的线程数量达到corePoolSize,且任务队列已满时:
    • 若线程数小于maximumPoolSize,则创建新线程执行任务。
    • 若线程数已达到maximumPoolSize,则执行拒绝策略,抛出异常或根据策略处理任务。

代码示例:

private static void createThreadPool() {
    ExecutorService executorService = new ThreadPoolExecutor(2, 10,
            1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(5, true),
            Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
    for (int i = 0; i < 10; i++) {
        final int index = i;
        executorService.execute(() -> {
            // 获取线程名称,默认格式:pool-1-thread-1
            System.out.println(DateUtil.now() + " " + Thread.currentThread().getName() + " " + index);
            // 等待 2 秒
            sleep(2000);
        });
    }
}

执行效果:

ThreadPoolExecutor 示例

由于核心线程数为 2,任务队列大小为 5,线程存活时间为 1 分钟,执行流程如下:

  • 当任务 0 和任务 1 到来时,会创建 2 个核心线程执行它们。
  • 任务 2 至任务 6 到来时,由于核心线程已满,且队列未满,这些任务会进入任务队列等待。
  • 任务 7 至任务 9 到来时,由于核心线程已满,且任务队列也已满,因此会创建额外的 3 个线程来执行这些任务。
  • 值得注意的是,任务 7-9 可能会比任务 2-6(队列中的任务)更早执行,这取决于任务的执行速度和新线程创建的速度。
  • 由于每个任务仅需 2 秒,而线程存活时间为 1 分钟,线程会被复用,最终总共创建了 5 个线程(2 个核心线程 + 3 个非核心线程)。

三、Executors工厂类方法的底层实现与使用建议

虽然我们讨论了五种线程池的创建方式,但本质上可以归结为两类。Executors工厂类提供的四种便捷方法,其底层均是通过ThreadPoolExecutor类来实现的。换句话说,Executors通过预设ThreadPoolExecutor的不同参数组合,为特定场景提供了封装好的线程池。下面我们将通过源码分析来揭示这一点:

1. newCachedThreadPool的实现

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

由于使用了SynchronousQueue,该队列不存储元素,任务会直接提交给线程执行,这相当于一个队列大小为 0 的队列。同时,最大线程数设置为Integer.MAX_VALUE,这意味着当线程不足时,会无限创建新线程。当线程空闲 60 秒后,会被回收,从而实现了可缓存线程池的特性。

2. newFixedThreadPool的实现

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

由于corePoolSizemaximumPoolSize均设置为nThreads,线程池的线程数量是固定的。同时,使用了无界队列LinkedBlockingQueue,这意味着所有多余的任务都会进入队列排队,从而实现了一个固定大小、并发数量可控的线程池。

3. newScheduledThreadPool的实现

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

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

其核心在于使用了DelayedWorkQueue,这是一个延迟队列,只有当元素的延迟期满后才能从中提取。这使得线程池能够实现定时执行任务的功能。至于周期性执行,则是通过ScheduledExecutorService类中scheduleAtFixedRate等方法的上层封装来实现的。

4. newSingleThreadExecutor的实现

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

由于corePoolSizemaximumPoolSize均设置为 1,线程池始终只有一个工作线程。同时,使用了无界队列LinkedBlockingQueue,这意味着所有多余的任务都会进入队列排队,从而实现了一个单线程按指定顺序执行的线程池。

阿里巴巴《Java 开发手册》建议

尽管Executors工厂类提供的封装方法看似简化了线程池的使用,但根据《阿里巴巴 Java 开发手册》的强制规范,明确不建议直接使用Executors类创建线程池:

【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

Executors返回的线程池对象的弊端如下:

FixedThreadPoolSingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM

CachedThreadPoolScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM

通过上述源码分析,我们可以验证此规范的合理性。因此,建议开发者直接使用ThreadPoolExecutor类来创建线程池,并根据具体应用场景合理配置各项参数,以规避潜在的资源耗尽风险。

posted @ 2025-11-26 22:41  Higurashi-kagome  阅读(158)  评论(0)    收藏  举报