1. 为什么需要线程池?
在 Java 并发编程中,线程的创建和销毁是一项昂贵的操作。频繁地创建和销毁线程会带来较高的系统开销,甚至可能因线程数过多而导致 OOM(OutOfMemoryError) 或 CPU 过载。
线程池(Thread Pool) 的设计初衷正是为了解决这些问题。
线程池的主要优点:
- 降低资源消耗:通过复用线程,避免频繁的创建和销毁。
- 提高响应速度:任务到达时可复用现有线程,无需等待线程创建。
- 增强可管理性:提供任务队列和线程管理能力,防止资源耗尽。
2. 线程池的核心组成
Java 提供了 ThreadPoolExecutor
作为线程池的核心实现,构造方法包含多个参数,主要负责线程池的各种行为控制:
2.1. 构造方法(7 个参数)
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
参数详解:
参数名 | 说明 |
---|---|
corePoolSize | 核心线程数,线程池始终保持的最小线程数量。 |
maximumPoolSize | 最大线程数,线程池可创建的最大线程数。 |
keepAliveTime | 非核心线程空闲超过该时间会被销毁。 |
unit | keepAliveTime 的时间单位。 |
workQueue | 用于存放等待执行的任务。 |
threadFactory | 创建新线程的工厂,可自定义线程属性。 |
handler | 当线程池无法接收新任务时的拒绝策略。 |
示例:创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
- 核心线程数为 2,最大线程数为 4
- 多余线程在空闲 60 秒后销毁
- 任务队列最多容纳 10 个任务
- 使用默认线程工厂创建线程
- 拒绝策略为
AbortPolicy
,即直接抛出异常
3. 创建线程池的方式
3.1. 使用 Executors
创建线程池
Java 提供了 Executors
工具类,可以快速创建不同类型的线程池:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
线程池类型 | 创建方法 | 特点 |
---|---|---|
固定大小线程池 | newFixedThreadPool(n) | 线程数固定,适合长期任务。 |
缓存线程池 | newCachedThreadPool() | 线程数不固定,适合短期任务。 |
单线程池 | newSingleThreadExecutor() | 只有一个线程,任务顺序执行。 |
调度线程池 | newScheduledThreadPool(n) | 支持定时和周期性任务。 |
3.2. 推荐使用 ThreadPoolExecutor
创建线程池
尽管 Executors
提供了便捷的方法,但其内部参数可能存在潜在风险,例如 无界队列可能导致 OOM。因此,推荐显式使用 ThreadPoolExecutor
并自定义参数:
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
4. 线程池的任务执行流程
线程池任务执行过程可以分为以下几个阶段:
4.1. 任务执行流程说明
- 线程数 < 核心线程数(corePoolSize):创建新线程执行任务。
- 线程数达到核心线程数,任务进入任务队列,等待执行。
- 任务队列已满,且线程数小于最大线程数(maximumPoolSize):创建新线程执行任务。
- 线程数达到 maximumPoolSize 且任务队列已满:执行拒绝策略。
4.2. 线程池中的两阶段回收机制
线程池中的线程回收遵循两阶段机制:
- 核心线程(corePoolSize 内的线程)
核心线程默认情况下是长期存活的,除非显式调用allowCoreThreadTimeOut(true)
,才会在空闲超过keepAliveTime
后被回收。
executor.allowCoreThreadTimeOut(true);
- 非核心线程(超过 corePoolSize 的线程)
非核心线程会在空闲超过keepAliveTime
后自动销毁,防止资源浪费。
5. 线程池的拒绝策略
当任务无法被线程池接受时,ThreadPoolExecutor
提供了 4 种内置拒绝策略:
拒绝策略 | 描述 |
---|---|
AbortPolicy | 抛出 RejectedExecutionException (默认策略)。 |
CallerRunsPolicy | 由提交任务的线程执行该任务,减轻线程池压力。 |
DiscardPolicy | 丢弃该任务,不抛出异常。 |
DiscardOldestPolicy | 丢弃队列中最早的任务,尝试执行当前任务。 |
示例:
new ThreadPoolExecutor(2, 4, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
6. 常见问题与优化建议
- 避免使用
Executors
创建线程池,推荐显式参数化ThreadPoolExecutor
。 - 合理配置
corePoolSize
和maximumPoolSize
:- CPU 密集型任务(如计算):
corePoolSize = CPU 核数 + 1
- I/O 密集型任务(如数据库操作):
corePoolSize = CPU 核数 * 2
- CPU 密集型任务(如计算):
- 选择合适的任务队列:
LinkedBlockingQueue
:适用于任务较多时,防止任务丢失。SynchronousQueue
:适用于高吞吐、低延迟场景。
- 监控线程池状态:
System.out.println("Active threads: " + threadPool.getActiveCount());
System.out.println("Task queue size: " + threadPool.getQueue().size());
- 避免线程池泄漏:执行完任务后,调用
shutdown()
或shutdownNow()
释放资源。
7. 总结
- 线程池能有效提升并发性能,减少系统资源开销。
Executors
提供便捷的方法,但推荐使用ThreadPoolExecutor
来显式配置线程池参数。- 合理配置线程池参数,并根据任务类型优化,是高效并发编程的关键。