Java中线程池的基本原理&执行过程


Java中线程池的基本原理&执行过程

在Java中,我们创建线程池之后,在线程池执行代码过程中它的主要流程是按怎么个逻辑来的?

重点:

线程池是一种池化技术,用于预先创建并管理一组线程,主要为了避免频繁创建和销毁线程的开销,提高性能和响应速度。

创建线程池的几个关键参数配置:核心线程数、最大线程数(数量上包括核心线程数的数量)、空闲(线程)存活时间、工作队列、拒绝策略。

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(20, // 核心线程数
                300, // 最大线程数
                60, // 空闲(线程)存活时间 60
                TimeUnit.SECONDS, // 空闲(线程)存活时间的单位
                new ArrayBlockingQueue<>(10), // 工作队列
                Executors.defaultThreadFactory(), // 线程工厂,用于创建新线程
                new ThreadPoolExecutor.CallerRunsPolicy());// 拒绝策略:任务拒绝处理器(RejectedExecutionHandler),当任务无法执行时的处理策略
示例参数说明:
// 核心线程数,线程池会一直维护的线程数量,即使这些线程处于空闲状态,也不会被回收
        int corePoolSize = 2;
        // 最大线程数,线程池允许存在的最大线程数量,包括核心线程和非核心线程
        int maximumPoolSize = 4;
        // 非核心线程的空闲存活时间,即当非核心线程处于空闲状态超过这个时间,该线程会被回收
        long keepAliveTime = 10;
        // 时间单位,用于指定 keepAliveTime 的时间单位,例如 TimeUnit.SECONDS 表示秒
        TimeUnit unit = TimeUnit.SECONDS;
        // 任务等待队列,用于存储等待执行的任务,当核心线程都在执行任务时,新任务会先进入此队列等待
        BlockingQueue<Runnable> workQueue = new java.util.concurrent.LinkedBlockingQueue<>();
        // 线程工厂,用于创建新线程,可自定义线程的属性,如名称、优先级、是否为守护线程等
        ThreadFactory threadFactory = Executors.defaultThreadFactory();
        // 拒绝策略,当任务队列已满且线程池中的线程数达到最大线程数时,用于处理新提交的任务,例如抛出异常、丢弃任务等
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                // 核心线程数
                corePoolSize,
                // 最大线程数
                maximumPoolSize,
                // 非核心线程的空闲存活时间
                keepAliveTime,
                // 时间单位
                unit,
                // 任务等待队列
                workQueue,
                // 线程工厂
                threadFactory,
                // 拒绝策略
                handler);

主要工作原理:

  1. 默认情况下线程不会预创建,任务提交之后才会创建线程(需要的话可以通过设置 prestartAllCoreThreads 来预创建核心线程)。
  2. 当所有的核心线程都处于占用时,也就是核心线程满了之后,此时不会再新建线程,而是把任务堆积到任务队列中。
  3. (工作队列可以看作是用来一个存放提交的任务的集合)如果工作队列放不下了,这时候才会新增线程,创建新的线程,直至达到设置的最大线程数。
  4. 如果工作队列满了,然后也达到了最大线程数,这时候线程池再收到任务时,就会执行拒绝策略,对此时的任务进行设置的策略方案进行处理。
  5. 如果创建的线程中存在空闲的时间超过设置的空闲存活时间,并且当前的线程数量大于核心线程数,此时会开始销毁线程。当然不会一股脑的销毁,销毁的是满足前面2个条件的线程,并且当线程数等于核心线程数时就不再进行销毁。(如果设置 allowCoreThreadTimeOut 为 true 也可以回收核心线程数,默认的是为 falise)

注意

创建线程池中设置的参数,核心线程数--核心线程和最大线程数--非核心线程这2者没有什么特殊区分,都是创建的线程。主要线程的创建顺序不同。

工作队列的类型:
  • SynchronousQueue:不存储任务,直接将任务提交给线程。
  • LinkedBlockingQueue:链表结构的阻塞队列,大小无限。
  • ArrayBlockingQueue:数组结构的有界阻塞队列。
  • PriorityBlockingQueue:带优先级的无界阻塞队列。

线程池中的拒绝策略:

  • CallerRunsPolicy:new ThreadPoolExecutor.CallerRunsPolicy();当任务队列满且没有线程空闲,此时添加任务由即调用者线程执行。适用于希望通过减缓任务提交速度来稳定系统的场景。
  • AbortPolicy:new ThreadPoolExecutor.AbortPolicy();当任务队列满且没有线程空闲,此时添加任务会直接抛出 RejectedExecution错误,这也是默认的拒绝策略。适用于必须通知调用者任务未能被执行的场景。
  • DiscardOldestPolicy:new ThreadPoolExecutor.DiscardOldestPolicy();当任务队列满且没有线程空闲,会删除最早的任务,然后重新提交当前任务。适用于希望丢弃最旧的任务以保证新的重要任务能够被处理的场景。
  • DiscardPolicy:new ThreadPoolExecutor.DiscardPolicy();直接丢弃当前提交的任务,不会执行任何操作,也不抛出异常。适用于对部分任务丢弃没有影响的场景,或者系统负载较高时不需要处理所有任务。

自定义拒绝策略

可以实现 RejectedExecutionHandler 接口来定义自定义的拒绝策略。例如,在自定义的策略里记录日志或者将任务重新排队。

public class CustomRejectedExecutionHandle implements RejectedExecutionHandler {

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 在 rejectedExecution 自己实现逻辑,比如记录日志、其他的拒绝逻辑
        System.out.println("自定义的决绝策略: " + r.toString() );
    }
}

提问:

1.为什么线程池达到核心线程数时要先使用阻塞队列存放任务,队列任务满了再创建新线程,而不是直接新增线程?

原因:

每创建一个线程都会占用一定的系统资源(如栈空间、线程调度开销等),直接增加线程会迅速消耗系统资源,导致性能下降。

使用阻塞队列可以将任务暂存,避免线程数量无限增长,确保资源利用率更高。

如果阻塞队列都满了,说明此时系统负载很大,再去增加线程到最大线程数去消化任务即可。

创建线程也是有成本的,特别是在系统处于负载大的场景下。

2.创建的线程池在处理任务时,有一个参数是BlockingQueue workQueue,workQueue存放的任务是由核心线程来处理,还是由非核心线程来处理?

在Java的线程池中,BlockingQueue<Runnable> workQueue用于存放待处理的任务。关于任务是由核心线程还是非核心线程处理的问题,我们需要理解线程池的工作机制。

线程池中的线程分为核心线程和非核心线程(也称为最大线程)。核心线程数由corePoolSize参数指定,最大线程数由maximumPoolSize参数指定。

线程池处理任务的基本流程如下:

\1. 当提交一个新任务时,如果当前运行的线程数小于核心线程数(corePoolSize),即使存在空闲线程,线程池也会创建一个新的核心线程来处理任务。

\2. 如果当前运行的线程数等于或大于核心线程数,新提交的任务会被放入工作队列(workQueue)中等待。

\3. 如果工作队列已满,且当前运行的线程数小于最大线程数(maximumPoolSize),则线程池会创建新的非核心线程来处理任务。

\4. 如果工作队列已满,且当前线程数已达到最大线程数,则根据拒绝策略(RejectedExecutionHandler)来处理新任务。

关键点:

- 核心线程和非核心线程在创建后并没有本质区别,它们都会从工作队列中获取任务来执行。

- 区别在于回收机制:核心线程默认会一直存活(即使空闲),而非核心线程在空闲一段时间后(由keepAliveTime参数指定)会被回收。

因此,工作队列中的任务既可能由核心线程处理,也可能由非核心线程处理。具体来说,当有线程(无论是核心还是非核心)空闲时,它就会从工作队列中取出任务来执行。

总结:任务被放入工作队列后,哪个线程(核心或非核心)空闲了,哪个线程就会从队列中取出任务并执行。所以,工作队列中的任务并不指定由核心线程或非核心线程处理,而是由所有线程(包括核心和非核心)共同竞争获取任务执行。

但是,需要注意的是,线程池在创建新线程(核心或非核心)时,都是用来处理新提交的任务(当核心线程都在忙且队列未满时任务入队,队列满时才创建非核心线程)。一旦线程被创建,它们就会不断地从工作队列中取任务执行,直到线程池关闭。

因此,你的问题:workQueue中的任务既可能由核心线程处理,也可能由非核心线程处理。这取决于哪个线程空闲。

在Java线程池中,BlockingQueue<Runnable> workQueue 存放的任务既可能由核心线程处理,也可能由非核心线程处理,具体取决于线程池的实时状态。以下是详细规则:

线程池处理任务的流程

  1. 核心线程优先处理
    • 当任务提交时,若当前运行的线程数 小于 corePoolSize,线程池会立即创建新的核心线程处理该任务(此时任务不会进入队列)。
  2. 任务入队条件
    • 若运行的线程数 等于或超过 corePoolSize,新任务会被放入 workQueue 等待。
  3. 非核心线程的触发条件
    • workQueue 已满,且运行的线程数 小于 maximumPoolSize,线程池会创建非核心线程处理队列中的任务。
  4. 拒绝策略
    • 若队列已满且线程数达到 maximumPoolSize,新任务会被拒绝(触发 RejectedExecutionHandler)。

关键结论

  • 队列中的任务由谁处理? workQueue 中的任务会被所有空闲线程(包括核心和非核心)竞争获取并执行。 例如:
    • 若核心线程空闲,则核心线程从队列中拉取任务执行。
    • 若核心线程繁忙但存在空闲的非核心线程,非核心线程会处理队列中的任务。
  • 非核心线程何时被回收? 非核心线程在空闲时间超过 keepAliveTime 后会被销毁,而核心线程默认长期存活(可通过 allowCoreThreadTimeOut(true) 设置核心线程超时回收)。

示例流程

假设线程池配置:corePoolSize=2, maximumPoolSize=4, workQueue容量=10

  1. 提交第1个任务 → 创建核心线程1执行(未入队)。
  2. 提交第2个任务 → 创建核心线程2执行(未入队)。
  3. 提交第3个任务 → 队列未满,任务入队等待。
  4. 若核心线程1空闲,则核心线程1从队列拉取第3个任务执行。
  5. 若队列满后提交第15个任务 → 创建非核心线程3处理队列中的任务。

总结

  • workQueue所有线程(核心+非核心)共享的任务源
  • 线程池优先使用核心线程,但队列任务的实际处理者取决于空闲线程的类型。
  • 非核心线程仅在队列满且线程数未达上限时创建,用于协助处理队列任务。

通过这种设计,线程池在保证核心线程高效处理的同时,通过动态扩展非核心线程应对突发负载,最后通过队列缓冲平稳调度任务。

JAVA-技能点
知识点
多线程