java 线程池

在 Java 的线程池中,线程池数量从 10 变回 5 的场景发生在线程池中临时增加的非核心线程(超过核心线程数的线程)空闲超过指定时间后被回收的情况下。具体来说,这个过程涉及到线程池的线程创建、任务处理和线程回收机制。下面详细分析 Java 线程池的工作原理,特别是线程池数量从 10 变回 5 的场景。

一、线程池工作原理概述

Java 中线程池的实现主要依赖于 ThreadPoolExecutor 类。线程池的两个关键参数是核心线程数(corePoolSize)和最大线程数(maximumPoolSize):

  • 核心线程数:线程池维护的最小线程数,即使线程空闲也不会被回收。在这里是 5。
  • 最大线程数:线程池可以创建的最大线程数,包括核心线程和临时线程。在这里是 10。

线程池的工作原理大致如下:

  1. 任务提交:当任务被提交到线程池时,如果核心线程数未满,线程池会优先创建新的核心线程处理任务。
  2. 任务队列:如果核心线程数已满,任务会被放入任务队列等待执行。
  3. 非核心线程:当核心线程数已满且任务队列也已满,线程池会继续创建新的非核心线程(即超出核心线程数的线程)来处理任务,直到达到最大线程数。
  4. 线程回收:当线程池的线程数量超过核心线程数,并且这些超出核心线程的线程处于空闲状态超过指定的时间(keepAliveTime),这些非核心线程会被逐步回收,直到线程池的线程数减少到核心线程数为止。

二、线程池数量从 10 变回 5 的场景

根据线程池的设计,线程池中的线程数量会动态调整:

  • 当有大量任务提交时,线程池会创建额外的线程来处理任务,最多可以扩展到 10 个线程。
  • 当任务处理完成,非核心线程处于空闲状态时,经过 keepAliveTime 时间后,线程池会回收非核心线程,逐渐将线程数量降到核心线程数,即 5。

因此,线程池数量从 10 变回 5 的场景发生在以下条件满足的情况下:

  1. 线程池中的所有任务都已处理完毕,没有新的任务提交。
  2. 超过核心线程数的 5 个临时线程处于空闲状态,并且空闲时间超过 keepAliveTime
  3. 线程池开始回收这些空闲的临时线程,将线程数量逐步减少到核心线程数 5。

三、线程池数量从 10 变回 5 的详细执行过程

假设核心线程数为 5,最大线程数为 10,keepAliveTime 为 60 秒,任务队列类型为 LinkedBlockingQueue(容量有限的队列)。

  1. 初始任务提交
    • 当有任务提交到线程池时,如果当前线程池中线程数量小于 5,线程池会优先创建新的核心线程来处理任务,直到线程数达到 5。
    • 如果任务数量继续增加且超过任务队列的容量(任务队列已满),线程池会进一步创建非核心线程(临时线程)来处理这些任务,直到线程池的线程数量达到最大线程数 10。
  2. 临时线程的创建
    • 在任务高峰期,线程池中的线程数量被扩展到最大线程数 10。
    • 此时,线程池中的 5 个核心线程和 5 个非核心线程(临时线程)共同处理任务。
  3. 任务处理完成,非核心线程进入空闲状态
    • 随着任务处理完毕,线程池中临时增加的非核心线程(即超过核心线程数的 5 个线程)进入空闲状态。
    • 非核心线程的空闲计时开始,如果在 keepAliveTime 时间内没有新任务提交到线程池,非核心线程会超时。
  4. 非核心线程的回收
    • 一旦非核心线程空闲超过 keepAliveTime(60 秒),线程池会回收这些非核心线程。
    • ThreadPoolExecutor 通过一个内部的空闲计时器逐步释放空闲的非核心线程。每次检查时,如果检测到某个非核心线程空闲超时,线程池将其从线程池中移除并终止。
    • 这一过程会持续进行,直到线程池中的线程数量减少到核心线程数 5。

四、实现原理:线程池如何自动回收非核心线程

ThreadPoolExecutor 中,线程池的回收机制是通过 keepAliveTimeallowCoreThreadTimeOut 配置来管理的。

  • keepAliveTime:设置了非核心线程的最大空闲时间,单位可以是秒、毫秒等。非核心线程空闲超过 keepAliveTime 时会被回收。
  • allowCoreThreadTimeOut:默认情况下,核心线程即使空闲也不会被回收。如果将 allowCoreThreadTimeOut 设置为 true,那么核心线程空闲时间超过 keepAliveTime 也会被回收。

每个非核心线程在空闲时都会进行空闲时间计时,超过 keepAliveTime 的非核心线程会被逐个回收,从而减少线程池中的线程数量,直到达到核心线程数。

五、代码示例

以下是一个模拟核心线程数为 5,最大线程数为 10,keepAliveTime 为 60 秒的线程池示例。

javaCopy codeimport java.util.concurrent.*;

public class ThreadPoolExample {

    public static void main(String[] args) throws InterruptedException {
        // 创建线程池,核心线程数为 5,最大线程数为 10,非核心线程的 keepAliveTime 为 60 秒
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                5, 
                10, 
                60, 
                TimeUnit.SECONDS, 
                new LinkedBlockingQueue<>(100)
        );

        // 提交大量任务使线程池扩展至最大线程数
        for (int i = 0; i < 20; i++) {
            executor.submit(() -> {
                try {
                    System.out.println("Executing task by " + Thread.currentThread().getName());
                    Thread.sleep(2000); // 模拟任务处理
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        // 等待一段时间,确保任务处理完成,非核心线程进入空闲状态
        Thread.sleep(3000);

        // 查看线程池的当前线程数
        System.out.println("Current Pool Size (After tasks): " + executor.getPoolSize());

        // 等待超过 keepAliveTime(60 秒),非核心线程将被回收
        Thread.sleep(70000); // 等待 70 秒

        // 查看线程池的当前线程数,预计会降至核心线程数 5
        System.out.println("Current Pool Size (After idle): " + executor.getPoolSize());

        // 关闭线程池
        executor.shutdown();
    }
}

六、总结

Java 线程池的线程数量从 10 变回 5 的过程是通过 keepAliveTime 控制非核心线程的空闲时间。当线程池中的任务减少,非核心线程处于空闲状态且空闲时间超过 keepAliveTime 时,线程池会逐步回收这些空闲的非核心线程,将线程数量减少到核心线程数,从而达到减少资源占用的效果。

4o

You said:

线程池种类有哪些

ChatGPT said:

ChatGPT

Java 提供了多种线程池实现,每种线程池适用于不同的应用场景。主要的线程池类型包括:

  1. 固定大小线程池(Fixed Thread Pool)
    • 使用 Executors.newFixedThreadPool(int nThreads) 创建,线程池的线程数量固定,适用于执行长期任务,减少线程创建的开销。
    • 特点:
      • 核心线程数和最大线程数相等,线程数固定。
      • 任务队列采用无界队列 LinkedBlockingQueue,当线程都在忙时,新任务会在队列中等待。
      • 适用于需要控制并发线程数量,且执行时间较长的任务。
  2. 缓存线程池(Cached Thread Pool)
    • 使用 Executors.newCachedThreadPool() 创建,线程数量不固定,根据任务需求动态调整线程数量。
    • 特点:
      • 核心线程数为 0,最大线程数为 Integer.MAX_VALUE
      • 任务队列采用 SynchronousQueue,新任务会直接创建新线程处理,空闲线程超过 60 秒会被回收。
      • 适用于短期大量并发任务,不确定线程数的场景。
  3. 单线程线程池(Single Thread Executor)
    • 使用 Executors.newSingleThreadExecutor() 创建,线程池中只有一个线程,所有任务按照顺序执行。
    • 特点:
      • 核心线程数和最大线程数均为 1,任务按顺序执行。
      • 保证任务在同一线程中按顺序执行,适用于串行执行任务且需要保持顺序的场景。
  4. 定时线程池(Scheduled Thread Pool)
    • 使用 Executors.newScheduledThreadPool(int corePoolSize) 创建,用于执行延迟任务或周期性任务。
    • 特点:
      • 核心线程数固定,最大线程数为 Integer.MAX_VALUE
      • 使用 DelayedWorkQueue 任务队列,支持延迟执行和周期性执行。
      • 适用于需要定时执行任务或周期性任务的场景,例如定时任务调度。
  5. 工作窃取线程池(Work Stealing Pool)
    • 使用 Executors.newWorkStealingPool(int parallelism) 创建,基于 ForkJoinPool 实现,适合并行计算任务。
    • 特点:
      • 线程池的线程数等于 CPU 的核心数(默认),可以手动指定。
      • 每个线程有自己的任务队列,支持工作窃取(当某线程的队列空闲时可以“窃取”其他线程的任务)。
      • 适用于需要提高吞吐量、分而治之的并行计算任务。

各种线程池的比较

线程池类型核心线程数最大线程数适用场景
Fixed Thread Pool固定固定长期任务,控制并发线程数量
Cached Thread Pool0Integer.MAX_VALUE短期大量并发任务,适合不确定线程数
Single Thread Executor11按顺序执行任务,保持顺序的串行任务
Scheduled Thread Pool固定Integer.MAX_VALUE定时任务或周期性任务
Work Stealing PoolCPU 核心数(默认)Integer.MAX_VALUE并行计算,提高吞吐量

线程池选择建议

  • 固定大小线程池:适合控制并发数的应用,避免过多线程导致系统资源被耗尽。
  • 缓存线程池:适合执行短期异步任务,任务量不确定时灵活创建线程。
  • 单线程池:适合需要顺序执行的任务,例如日志写入、文件操作。
  • 定时线程池:适合周期性执行任务或需要延时执行的场景,例如定时清理缓存、定期备份。
  • 工作窃取线程池:适合分而治之的并行计算任务,如数据处理、大型集合计算。

4o

You said:

线程池拒绝策略有哪些?

ChatGPT said:

ChatGPT

在 Java 的 ThreadPoolExecutor 中,当线程池的任务队列已满、线程数量已达到最大线程数,且无法再接受新的任务时,会触发拒绝策略(RejectedExecutionHandler)。拒绝策略用于定义如何处理新提交但无法执行的任务。Java 提供了以下几种内置的拒绝策略:

  1. AbortPolicy(默认策略)
    • 策略说明:直接抛出 RejectedExecutionException 异常,通知调用者任务被拒绝。
    • 适用场景:适用于必须执行所有任务的场景,以确保程序知道任务被拒绝,适合无法丢弃任何任务的关键业务场景。
    • 代码示例:javaCopy codeThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<>(queueCapacity), new ThreadPoolExecutor.AbortPolicy() );
  2. DiscardPolicy
    • 策略说明:直接丢弃无法执行的新任务,不抛出任何异常,也不会通知任务提交方。
    • 适用场景:适用于不敏感的任务场景,适合允许丢弃部分任务的情况,例如日志或监控信息的处理等。
    • 代码示例:javaCopy codeThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<>(queueCapacity), new ThreadPoolExecutor.DiscardPolicy() );
  3. DiscardOldestPolicy
    • 策略说明:丢弃任务队列中最旧的任务(即队列头部任务),然后重新尝试提交当前任务。
    • 适用场景:适用于需要保证新任务执行优先级的场景,适合队列中的任务相对过时或可以被丢弃的情况。
    • 代码示例:javaCopy codeThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<>(queueCapacity), new ThreadPoolExecutor.DiscardOldestPolicy() );
  4. CallerRunsPolicy
    • 策略说明:由调用线程(即提交任务的线程)直接执行当前任务,避免任务丢失,但会影响调用线程的性能。
    • 适用场景:适用于允许任务回退到调用线程执行的情况,可以用于控制提交速率,使得当任务队列满时,调用线程的执行速度减慢,间接减小线程池的负载。
    • 代码示例:javaCopy codeThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<>(queueCapacity), new ThreadPoolExecutor.CallerRunsPolicy() );

自定义拒绝策略

除了内置的拒绝策略,ThreadPoolExecutor 允许实现 RejectedExecutionHandler 接口来自定义拒绝策略。例如,可以记录拒绝的任务日志、进行限流控制、触发告警等。

javaCopy codepublic class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 自定义处理逻辑,例如记录日志或告警
        System.out.println("Task " + r.toString() + " rejected from " + executor.toString());
    }
}

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize, 
    maximumPoolSize, 
    keepAliveTime, 
    TimeUnit.SECONDS, 
    new LinkedBlockingQueue<>(queueCapacity), 
    new CustomRejectedExecutionHandler()
);

拒绝策略选择建议

  • AbortPolicy:适合需要严格控制所有任务执行的场景,通过抛出异常使调用方明确任务未执行。
  • DiscardPolicy:适用于不敏感的任务,例如一些临时性、不重要的日志任务。
  • DiscardOldestPolicy:适合队列中旧任务可以被丢弃而优先处理新任务的场景,例如频繁更新的监控数据。
  • CallerRunsPolicy:适合希望通过调用线程执行以控制任务提交速率的情况,适用于不要求线程池高吞吐量的场景。

4o

You said:

线程池刚创建,没有任务,会新建线程吗?

ChatGPT said:

ChatGPT

当线程池刚创建时,如果没有提交任何任务,那么线程池不会立即创建线程。线程池中的线程创建是按需进行的,即只有在有任务提交并需要线程执行时,线程池才会创建线程。

详细说明

  1. 核心线程的创建
    • 线程池的核心线程数(corePoolSize)定义了线程池在任务队列为空时维持的最小线程数量。
    • 但是,核心线程并不会在线程池创建时就立即启动。默认情况下,核心线程是延迟创建的,即只有在有任务提交时,线程池才会按需创建核心线程来执行任务。
  2. 线程的按需创建
    • 当任务被提交到线程池时,线程池首先会尝试创建核心线程。如果核心线程数已满,任务会被放入任务队列中。
    • 只有当任务队列也满了时,线程池才会继续创建超出核心线程数的非核心线程,直到达到最大线程数(maximumPoolSize)。
  3. 预启动核心线程
    • 尽管线程池默认是按需创建线程,但可以调用 prestartCoreThread()prestartAllCoreThreads() 方法来提前启动核心线程。
      • prestartCoreThread():启动一个核心线程。
      • prestartAllCoreThreads():启动所有核心线程。
    • 这样做可以确保在有任务到来前核心线程已经就绪。

示例代码

以下代码展示了默认情况下线程池不会在创建时启动线程,只有在提交任务时才会创建线程。

javaCopy codeimport java.util.concurrent.*;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个核心线程数为 2,最大线程数为 4 的线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, 4, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>());

        // 初始时线程池不会创建任何线程
        System.out.println("Initial pool size: " + executor.getPoolSize());

        // 提交一个任务后,线程池才会创建一个核心线程来执行任务
        executor.submit(() -> {
            System.out.println("Task is running");
        });

        // 查看线程池的当前线程数
        System.out.println("Pool size after submitting task: " + executor.getPoolSize());

        // 关闭线程池
        executor.shutdown();
    }
}

总结

  • 默认情况下,线程池在刚创建时不会立即创建线程。只有当有任务提交时,线程池才会创建线程来处理任务。
  • 可以使用 prestartCoreThread()prestartAllCoreThreads() 提前创建核心线程,确保线程池在任务到来之前已经有核心线程准备就绪。

4o

You said:

核心线程池数创建之后会一直存在吗?如何保活

ChatGPT said:

ChatGPT

在 Java 的 ThreadPoolExecutor 中,核心线程默认会一直存在,即使它们处于空闲状态,线程池也不会主动回收核心线程。核心线程的存在与线程池的配置密切相关,主要受以下因素影响:

  1. 核心线程的保活
    • 默认情况下,核心线程在没有任务执行时会保持活动状态,且不会被线程池回收。
    • 但是可以通过调用 ThreadPoolExecutor 的方法 allowCoreThreadTimeOut(true) 来改变这一行为,使核心线程在空闲时间超过 keepAliveTime 后被回收。此时,核心线程的保活行为将与非核心线程相同。
  2. 核心线程的保活机制
    • 当调用 allowCoreThreadTimeOut(true) 后,核心线程也会受到 keepAliveTime 参数的影响。
    • 具体来说,如果核心线程在空闲时间内没有被分配到新任务且空闲时间超过 keepAliveTime,线程池会将该核心线程终止并回收。
    • 这种配置适用于一些需要灵活管理资源的场景,例如在低负载时期减少系统资源占用。
  3. 如何设置核心线程保活
    • keepAliveTime:用于指定线程的最大空闲时间,单位可以是秒、毫秒等,默认为 0。
    • allowCoreThreadTimeOut(true):允许核心线程在空闲时间超过 keepAliveTime 后回收。

代码示例

以下示例展示了如何配置核心线程的保活时间以及回收机制:

javaCopy codeimport java.util.concurrent.*;

public class CoreThreadKeepAliveExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建核心线程数为 2,最大线程数为 4 的线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, 4, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>()
        );

        // 允许核心线程超时并被回收
        executor.allowCoreThreadTimeOut(true);

        // 提交两个任务,这将启动核心线程来执行任务
        executor.submit(() -> {
            System.out.println("Task 1 is running");
        });
        executor.submit(() -> {
            System.out.println("Task 2 is running");
        });

        // 等待所有任务完成
        Thread.sleep(5000);

        // 查看当前线程池的核心线程数
        System.out.println("Core pool size after tasks: " + executor.getPoolSize());

        // 等待超过 keepAliveTime(10 秒),观察核心线程是否被回收
        Thread.sleep(15000);

        // 查看核心线程数,预计此时会被回收,回到0
        System.out.println("Core pool size after idle: " + executor.getPoolSize());

        // 关闭线程池
        executor.shutdown();
    }
}

总结

  • 默认情况下,核心线程会一直存在,不会被线程池回收。
  • 开启核心线程超时:调用 allowCoreThreadTimeOut(true) 可以让核心线程在空闲时间超过 keepAliveTime 后被回收,适用于负载波动较大的场景。
  • 保活机制设置:通过设置 keepAliveTime 来控制空闲线程的保活时间,再结合 allowCoreThreadTimeOut 控制核心线程的保活行为。
0 0 投票数
Article Rating
订阅评论
提醒
guest
0 评论
最旧
最新 最多投票
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x