完全掌握线程池是每个Javaer必须要做的事情,线程池的知识不仅仅是面试中的八股 更是让使用者受益匪浅。
本文先从 ThreadPoolExecutor 开头的英文注释出发,搞清楚 Doug Lea (ThreadPoolExecutor 编写者)对使用者想说的话,加粗部分为我自己的补充。
这段代码是Java中ThreadPoolExecutor类的文档注释,主要描述了线程池的工作原理和配置方法。以下是主要内容的中文翻译:
-
线程池的作用:
- 线程池使用多个线程来执行提交的任务,通常通过
Executors工厂方法进行配置。 - 线程池解决了两个问题:提高大量异步任务的执行性能(由于减少了每个任务的调用开销),并提供了一种管理和限制资源(包括线程)消耗的方法。
- 每个
ThreadPoolExecutor还维护一些基本统计信息,例如已完成任务的数量。
- 线程池使用多个线程来执行提交的任务,通常通过
-
推荐使用工厂方法:
- 推荐使用
Executors工厂方法中的便捷方法,如newCachedThreadPool(无界线程池,带有自动线程回收)、newFixedThreadPool(固定大小线程池)和newSingleThreadExecutor(单个后台线程),这些方法预配置了最常见的使用场景。 - 如果需要手动配置和调整线程池,可以参考以下指南。
- 推荐使用
-
核心和最大线程数:
ThreadPoolExecutor会根据核心线程数(corePoolSize)和最大线程数(maximumPoolSize)自动调整线程池大小。- 当提交新任务时,如果运行中的线程数少于核心线程数,则创建新线程处理请求,即使其他工作线程处于空闲状态。
- 如果运行中的线程数超过核心线程数但小于最大线程数,只有当队列已满时才会创建新线程。
- 设置相同的核心线程数和最大线程数可以创建固定大小的线程池;将最大线程数设置为一个几乎无限的值(如
Integer.MAX_VALUE)可以允许任意数量的并发任务。(这是不推荐的,因为java的平台线程与操作系统线程是1:1的关系,过多的线程会导致cpu过多进行线程切换有上下文切换的开销,同时大量的线程创建也会导致内存紧张出现OOM! )
-
按需创建线程:
- 默认情况下,即使是核心线程也只在有新任务到达时才创建和启动,但可以通过
prestartCoreThread或prestartAllCoreThreads方法动态覆盖这一行为。 - 如果使用非空队列创建线程池,建议预先启动线程。
- 默认情况下,即使是核心线程也只在有新任务到达时才创建和启动,但可以通过
-
创建新线程:
- 新线程由
ThreadFactory创建,默认使用Executors.defaultThreadFactory,它创建的所有线程都在同一个ThreadGroup中,具有相同的优先级和非守护线程状态。 - 提供不同的
ThreadFactory可以更改线程的名称、线程组、优先级、守护状态等。 - 如果
ThreadFactory无法创建线程(返回null),执行器将继续运行,但可能无法执行任何任务。 - 线程应具备“modifyThread”权限,否则服务可能会降级。
- 新线程由
-
空闲时间:
- 如果当前线程数超过核心线程数,空闲时间超过
keepAliveTime的多余线程将被终止,以减少资源消耗。 - 此参数可以动态更改,使用
Long.MAX_VALUE禁用空闲线程的终止。 - 默认情况下,空闲策略仅适用于超过核心线程数的线程,但可以通过
allowCoreThreadTimeOut方法将其应用于核心线程。
- 如果当前线程数超过核心线程数,空闲时间超过
-
队列策略:
-
可以使用任何
BlockingQueue来传递和保存提交的任务,队列的使用与线程池大小相关:- 如果运行中的线程数少于核心线程数,执行器总是优先创建新线程而不是排队。
- 如果运行中的线程数等于或超过核心线程数,执行器总是优先排队请求而不是创建新线程。
- 如果请求无法排队且创建新线程会超过最大线程数,则任务将被拒绝。
-
常见的队列策略包括直接传递(
SynchronousQueue)、无界队列(LinkedBlockingQueue)和有界队列(ArrayBlockingQueue)。(SynchronousQueue 也被成为同步队列,该队列没有容量,当执行任务时必须要有可以调度的线程来执行否则会执行拒绝策略,在应用中通常配合CallerRunsPolicy 拒绝策略来满足系统快速响应的线程池, 剩下的Link 和 Array 主要是数据结构的区别,Link 也可以指定容量,这两个主要构造带有有缓冲区域的线程池,这类线程池不追求及时处理主要控制资源消耗)
-
-
拒绝策略:
-
当执行器关闭或达到最大线程数和队列容量时,新提交的任务将被拒绝。
-
四种预定义的拒绝策略:
- 默认策略
AbortPolicy抛出RejectedExecutionException。 CallerRunsPolicy由调用execute的线程运行任务,减慢新任务的提交速度。DiscardPolicy直接丢弃无法执行的任务。DiscardOldestPolicy丢弃队列中最旧的任务并重试执行。
- 默认策略
-
-
钩子方法:
- 提供了可重写的
beforeExecute和afterExecute方法,在任务执行前后调用,用于操作执行环境(如重新初始化ThreadLocal、收集统计信息、添加日志条目)。 - 还可以重写
terminated方法,在执行器完全终止后执行特殊处理。 - 如果钩子或回调方法抛出异常,内部工作线程可能会失败并突然终止。
- 提供了可重写的
-
队列维护:
getQueue方法允许访问工作队列,主要用于监控和调试。- 提供了
remove和purge方法,用于在大量已取消的任务中协助存储回收。
-
最终化:
- 如果线程池不再被引用且没有剩余线程,它将自动关闭。
- 为了确保未引用的线程池在用户忘记调用
shutdown时也能被回收,应设置适当的空闲时间,使用核心线程数量 为 0或启用allowCoreThreadTimeOut。(这段文字是关于线程池终止情况,如果线程池没有被执行shutdown 以及 核心线程没有结束的情况下 这会导致优雅关闭失败,因为jvm中仍然有用户线程未结束,Spring应用可以使用ThreadPoolTaskExecutor 该包装后的线程池实现了Spring 的 DisposableBean 接口 在spring应用销毁时调用 shutdown)
控制流图
flowchart TD
A[开始] --> B{线程池是否已关闭}
B -->|是| C[拒绝任务]
B -->|否| D{当前线程数小于核心线程数}
D -->|是| E[创建新线程执行任务]
D -->|否| F{任务加入队列}
F --> G{队列是否已满}
G -->|否| H[等待线程处理任务]
G -->|是| I{当前线程数小于最大线程数}
I -->|是| J[创建新线程执行任务]
I -->|否| K[拒绝任务]