线程池是 Java 并发编程中最重要的工具之一,它通过复用线程、控制并发数和任务队列机制,显著提高了多线程程序的性能和资源管理效率。本文将深入解析线程池的核心参数、工作流程,以及如何使用 Executors
工具类创建常见的线程池。
一、线程池的核心参数
Java 线程池的核心类是 ThreadPoolExecutor
,其构造函数包含以下关键参数:
参数名 | 作用 |
---|---|
corePoolSize | 核心线程数,即使线程空闲也不会被回收(除非设置 allowCoreThreadTimeOut )。 |
maximumPoolSize | 线程池允许的最大线程数。 |
keepAliveTime | 非核心线程空闲时的存活时间。 |
unit | 存活时间的单位(如秒、毫秒)。 |
workQueue | 任务队列,用于存放待执行的任务。 |
threadFactory | 线程工厂,用于创建新线程。 |
handler | 拒绝策略,当任务数超过系统承载时的处理方式。 |
二、线程池的工作流程
-
提交任务:调用
execute(Runnable command)
提交任务。 -
核心线程分配:若当前线程数 <
corePoolSize
,创建新线程执行任务。 -
任务入队:若核心线程已满,任务进入阻塞队列等待。
-
扩容线程池:若队列已满且线程数 <
maximumPoolSize
,创建新线程处理任务。 -
拒绝策略触发:若队列和线程池均满,触发拒绝策略(如抛出异常或丢弃任务)。
-
线程回收:当线程池中的线程数超过
corePoolSize
时,多余的线程在空闲时间超过keepAliveTime
后会被回收,直到线程数降到corePoolSize
。
三、使用 Executors
创建常见线程池
Executors
是 Java 提供的工具类,用于快速创建常见的线程池。以下是四种常用的线程池:
1. 固定大小线程池(FixedThreadPool)
特点
-
核心线程数 = 最大线程数。
-
任务队列无界(
LinkedBlockingQueue
)。 -
适用于负载较重的服务,如 Web 服务器处理请求。
创建方式
ExecutorService executor = Executors.newFixedThreadPool(5);
适用场景
-
需要限制线程数的场景。
-
任务执行时间较长,且任务数量较多。
2. 缓存线程池(CachedThreadPool)
特点
-
核心线程数为 0,最大线程数为
Integer.MAX_VALUE
。 -
任务队列为同步队列(
SynchronousQueue
),无容量。 -
适用于大量短生命周期任务,如 HTTP 请求处理。
创建方式
ExecutorService executor = Executors.newCachedThreadPool();
适用场景
-
任务执行时间短,且任务数量波动较大。
-
对线程数无严格限制的场景。
3. 单线程池(SingleThreadExecutor)
特点
-
核心线程数 = 最大线程数 = 1。
-
任务队列无界(
LinkedBlockingQueue
)。 -
保证所有任务按提交顺序执行,避免同步问题。
创建方式
ExecutorService executor = Executors.newSingleThreadExecutor();
适用场景
-
需要顺序执行任务的场景(如日志写入)。
-
单线程任务执行环境。
4. 定时任务线程池(ScheduledThreadPool)
特点
-
支持周期性任务或延迟任务。
-
任务队列为延迟队列(
DelayedWorkQueue
)。
创建方式
ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
示例:定时任务
executor.scheduleAtFixedRate(() -> { System.out.println("Task executed at: " + System.currentTimeMillis()); }, 0, 1, TimeUnit.SECONDS); // 初始延迟 0 秒,周期 1 秒
适用场景
-
定时任务调度(如心跳检测、数据同步)。
-
延迟任务执行。
四、线程池的拒绝策略
当任务数超过系统承载时,线程池会触发拒绝策略。Java 提供了四种内置策略:
策略类 | 行为 |
---|---|
AbortPolicy (默认) | 抛出 RejectedExecutionException 异常。 |
CallerRunsPolicy | 由提交任务的线程直接执行该任务。 |
DiscardPolicy | 静默丢弃被拒绝的任务。 |
DiscardOldestPolicy | 丢弃队列中最旧的任务,然后重新提交当前任务。 |
示例:自定义拒绝策略
ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10), Executors.defaultThreadFactory(), (r, executor) -> System.out.println("Task rejected: " + r) );
五、线程池的最佳实践
-
避免使用无界队列
无界队列(如LinkedBlockingQueue
)可能导致内存溢出。推荐使用有界队列(如ArrayBlockingQueue
)。 -
合理配置线程数
-
CPU 密集型任务:线程数 ≈ CPU 核心数。
-
I/O 密集型任务:线程数 ≈ CPU 核心数 * (1 + 平均等待时间/平均计算时间)。
-
-
异常处理
任务中必须捕获异常,否则线程可能提前终止:executor.execute(() -> { try { // 业务代码 } catch (Exception e) { e.printStackTrace(); } });
-
关闭线程池
使用shutdown()
平滑关闭,或shutdownNow()
立即终止。
六、经典面试题
1. 线程池的核心参数有哪些?
-
核心线程数、最大线程数、存活时间、任务队列、线程工厂、拒绝策略。
2. FixedThreadPool 和 CachedThreadPool 的区别?
-
FixedThreadPool:固定线程数,适用于负载较重的场景。
-
CachedThreadPool:线程数动态调整,适用于短生命周期任务。
3. 如何选择合适的拒绝策略?
-
AbortPolicy:默认策略,抛出异常。
-
CallerRunsPolicy:由提交任务的线程执行,降低提交速度。
-
DiscardPolicy:静默丢弃任务。
-
DiscardOldestPolicy:丢弃最旧任务,适用于允许丢任务的场景。
4. 线程池的工作流程是什么?
-
提交任务 → 核心线程分配 → 任务入队 → 扩容线程池 → 触发拒绝策略→线程回收。
七、总结
线程池是 Java 并发编程的核心工具,合理配置线程池参数和选择拒绝策略,可以显著提高程序的性能和稳定性。通过 Executors
工具类,我们可以快速创建常见的线程池,但在实际开发中,建议根据业务需求自定义线程池参数,并结合监控工具(如 JConsole)观察线程池的运行状态。