线程池
✅ 线程池概述
在多线程编程中,创建和销毁线程是一项相对昂贵的操作。为了减少频繁创建和销毁线程带来的性能开销,线程池(Thread Pool)技术应运而生。线程池通过提前创建一定数量的线程来 复用线程,提高系统的 效率 和 性能。
线程池的主要优势:
- 避免频繁创建和销毁线程的开销。
- 可以控制线程的最大并发数,防止资源过度消耗。
- 线程池可以更好地管理线程生命周期,避免线程泄漏。
✅ Java 中的线程池
在 Java 中,线程池的使用通过 java.util.concurrent
包中的 Executor
接口及其实现类来完成。
1. 线程池核心接口:Executor
Executor
接口定义了执行异步任务的核心方法:execute(Runnable command)
。ExecutorService
是Executor
的子接口,提供了更多的线程池控制方法,如提交任务、关闭线程池等。
2. 常见线程池实现:
ThreadPoolExecutor
- 这是
ExecutorService
的最常用实现类,具有高度的自定义性。
- 这是
Executors
工厂类- 提供了几种常用的线程池实现,如 固定大小线程池、单线程池、可缓存线程池 等。
✅ 线程池的类型
1. 可缓存线程池(newCachedThreadPool()
)
- 特点:线程池的大小会根据实际情况调整,如果线程池中没有空闲线程,就会创建新线程,空闲线程会在60秒后被回收。
- 适用场景:适合执行大量短时间的小任务。
代码示例
1 |
|
- 解析:
newCachedThreadPool
创建一个可缓存线程池,根据任务量自动调整线程池的大小。
2. 固定大小线程池(newFixedThreadPool()
)
- 特点:线程池中的线程数固定,如果所有线程都处于忙碌状态,新的任务会被放入队列等待。
- 适用场景:适用于负载较重的任务,需要控制最大线程数的场景。
代码示例
1 |
|
- 解析:
newFixedThreadPool(3)
创建一个包含 3 个线程的线程池,超过 3 个任务会被放入队列中等待。
3. 单线程池(newSingleThreadExecutor()
)
- 特点:线程池只会创建一个线程,所有任务都会在该线程中顺序执行,适用于任务需要串行执行的场景。
- 适用场景:适用于需要确保任务顺序执行的场景。
代码示例
1 |
|
- 解析:
newSingleThreadExecutor
创建一个单线程池,所有任务会按顺序执行,不会并发。
4. 定时线程池(newScheduledThreadPool()
)
- 特点:线程池支持定时任务执行,可以周期性地执行任务。
- 适用场景:适合定时调度任务或周期性任务。
代码示例
1 |
|
- 解析:
scheduleAtFixedRate
用于按固定频率执行任务,适合定时调度任务。
✅ ThreadPoolExecutor
自定义线程池
1️⃣ 概述
ThreadPoolExecutor
是 Java 最强大的线程池实现类,允许我们自定义 核心线程数、最大线程数、任务队列、拒绝策略 等参数,从而精细控制线程池的行为。
在实际开发中,合理使用线程池可以提高系统吞吐量、降低资源消耗、防止线程无限创建导致 OOM(内存溢出)。
2️⃣ ThreadPoolExecutor
构造方法
完整的 ThreadPoolExecutor
构造方法包含 7 个参数:
1 |
|
参数 | 作用 |
---|---|
corePoolSize |
核心线程数 |
maximumPoolSize |
最大线程数 |
keepAliveTime |
非核心线程的存活时间 |
unit |
时间单位 |
workQueue |
任务队列(存放等待执行的任务) |
threadFactory |
线程工厂(可自定义线程名称等) |
handler |
拒绝策略(线程池满时如何处理新任务) |
3️⃣ 代码示例
1 |
|
4️⃣ 运行分析
🔹 线程池核心线程数 = 2,最大线程数 = 4,任务队列最多存 2 个任务:
- 前 2 个任务 直接由核心线程执行。
- 第 3、4 个任务 进入任务队列等待执行。
- 第 5、6 个任务 触发扩容,由最大线程数中的 2 个额外线程执行。
- 第 7 个任务 由于线程池 + 任务队列已满,根据
AbortPolicy
抛出异常。
5️⃣ 线程池拒绝策略(RejectedExecutionHandler
)
策略 | 描述 |
---|---|
AbortPolicy (默认) |
直接抛出 RejectedExecutionException ,任务被丢弃 🚨 |
CallerRunsPolicy |
由提交任务的线程(主线程)执行该任务,防止任务丢失 ✅ |
DiscardPolicy |
直接丢弃无法执行的任务,不抛异常 ❌ |
DiscardOldestPolicy |
丢弃任务队列中最老的任务,然后重新尝试提交新任务 🔄 |
📌 示例:使用 CallerRunsPolicy
(防止任务丢失)
1 |
|
🔹 适用场景:希望任务不丢失,但可能会影响主线程性能。
6️⃣ 线程池使用注意事项
✅ 合理设置 corePoolSize
和 maximumPoolSize
- CPU 密集型任务(计算型):
corePoolSize = CPU 核心数
- IO 密集型任务(磁盘/网络):
corePoolSize = 2 * CPU 核心数
✅ 选择合适的 BlockingQueue
队列类型 | 特点 |
---|---|
LinkedBlockingQueue |
无界队列,可能导致 OOM |
ArrayBlockingQueue |
有界队列,需要指定大小 |
SynchronousQueue |
不存储任务,提交任务必须有可用线程执行 |
✅ 合理使用 RejectedExecutionHandler
- 关键任务(不能丢失):
CallerRunsPolicy
- 非关键任务(允许丢弃):
DiscardPolicy
✅ 总结
线程池类型 | 特点 | 适用场景 |
---|---|---|
可缓存线程池 (newCachedThreadPool() ) |
根据需求动态增加线程,空闲线程会被回收 | 适合大量短时间任务 |
固定线程池 (newFixedThreadPool() ) |
线程数固定,多余的任务会在队列中等待 | 适合负载较重,控制线程数的场景 |
单线程池 (newSingleThreadExecutor() ) |
只有一个线程,任务顺序执行 | 需要保证任务顺序执行的场景 |
定时线程池 (newScheduledThreadPool() ) |
支持定时任务执行 | 定时任务、周期性任务 |
自定义线程池 (ThreadPoolExecutor ) |
高度可定制,灵活配置核心线程数、最大线程数等 | 需要精细控制线程池行为的场景 |
总结:选择线程池时,要根据任务的特性、负载要求和并发需求,来决定使用哪种类型的线程池。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Firefly!