线程池
为什么要用线程池?
线程池的核心作用是:复用线程、控制并发、统一管理任务。
不用线程池时,每来一个任务就创建一个线程,会有几个问题:
频繁创建和销毁线程开销大
线程创建涉及系统资源分配,销毁也有成本。
线程数量不可控
请求量大时可能创建大量线程,导致 CPU 频繁上下文切换,甚至内存溢出。
提高响应速度
线程池中已有线程,任务来了可以直接执行,不需要临时创建线程。
方便统一管理
可以控制最大线程数、任务队列、拒绝策略、线程命名、异常处理等。
线程池通过复用线程降低创建销毁成本,通过队列和最大线程数控制并发数量,从而提高系统稳定性和响应速度。
ThreadPoolExecutor 七大参数?
ThreadPoolExecutor 构造方法的七个核心参数是:
new ThreadPoolExecutor(
int corePoolSize, // 1. 核心线程数
int maximumPoolSize, // 2. 最大线程数
long keepAliveTime, // 3. 空闲线程存活时间
TimeUnit unit, // 4. 时间单位
BlockingQueue<Runnable> workQueue, // 5. 任务队列
ThreadFactory threadFactory, // 6. 线程工厂
RejectedExecutionHandler handler // 7. 拒绝策略
);1. corePoolSize:核心线程数
线程池长期保留的线程数量。
即使线程空闲,只要没有设置 allowCoreThreadTimeOut(true),核心线程一般不会被回收。
2. maximumPoolSize:最大线程数
线程池允许创建的最大线程数量。
当任务队列满了,并且当前线程数小于最大线程数时,会继续创建非核心线程。
3. keepAliveTime:空闲线程存活时间
非核心线程空闲超过这个时间后,会被回收。
如果设置了 allowCoreThreadTimeOut(true),核心线程也可以被回收。
4. unit:时间单位
keepAliveTime 的时间单位,比如:
TimeUnit.SECONDSTimeUnit.MILLISECONDS
5. workQueue:任务队列
当核心线程都在忙时,新任务会进入任务队列等待执行。
常见队列:
ArrayBlockingQueueLinkedBlockingQueueSynchronousQueuePriorityBlockingQueueDelayQueue
6. threadFactory:线程工厂
用于创建线程,可以自定义线程名称、是否守护线程、优先级等。
一般建议自定义线程名,方便排查问题。
ThreadFactory factory = r -> new Thread(r, "order-pool-" + index.getAndIncrement());7. handler:拒绝策略
当线程数达到最大线程数,并且队列也满了,再提交任务时触发拒绝策略。
线程池执行任务的流程?
线程池执行任务的核心流程是:
第一步:判断核心线程数是否已满
如果当前线程数 < corePoolSize,直接创建核心线程执行任务。
第二步:核心线程满了,任务进入队列
如果当前线程数 >= corePoolSize,任务会尝试放入 workQueue。
第三步:队列满了,尝试创建非核心线程
如果任务队列也满了,并且当前线程数 < maximumPoolSize,则创建非核心线程执行任务。
第四步:线程数也满了,执行拒绝策略
如果当前线程数已经达到 maximumPoolSize,并且队列也满了,就触发拒绝策略。
先用核心线程执行,核心线程满了就进队列,队列满了就创建非核心线程,线程数达到最大值后执行拒绝策略。
核心线程数怎么设置?
核心线程数没有固定答案,要看任务类型。
CPU 密集型任务
比如计算、加密、压缩、图片处理等,主要消耗 CPU。
推荐:
核心线程数 = CPU 核数 + 1或者:
核心线程数 = CPU 核数原因是线程太多会导致频繁上下文切换,反而降低性能。
IO 密集型任务
比如数据库操作、RPC 调用、文件读写、网络请求等,大部分时间在等待 IO。
推荐:
核心线程数 = CPU 核数 * 2或者更通用的估算公式:
线程数 = CPU 核数 * (1 + IO等待时间 / CPU计算时间)比如一个任务 80% 时间在等待 IO,20% 时间在计算:
线程数 = CPU 核数 * (1 + 80 / 20)
线程数 = CPU 核数 * 5实际生产建议
不要只靠公式,应该结合:
- CPU 使用率
- RT 响应时间
- QPS
- 队列堆积情况
- 内存占用
- 下游服务承载能力
CPU 密集型一般设置为 CPU 核数或 CPU 核数 + 1;IO 密集型可以适当调大,一般是 CPU 核数的 2 倍或根据等待时间比例估算,最终要通过压测确定。
拒绝策略有哪些?
JDK 默认提供四种拒绝策略。
1. AbortPolicy(默认)
直接抛出异常 RejectedExecutionException。
适合希望明确感知任务被拒绝的场景。
2. CallerRunsPolicy
由提交任务的线程自己执行任务。
比如主线程提交任务,如果线程池满了,就让主线程自己执行。
优点是可以降低任务提交速度,起到一定限流作用。
3. DiscardPolicy
直接丢弃新任务,不抛异常。
风险较高,因为任务会悄悄丢失。
4. DiscardOldestPolicy
丢弃队列中最老的任务,然后尝试提交当前任务。
也有任务丢失风险。
生产中常见做法
一般会自定义拒绝策略,比如:
- 打日志
- 记录监控指标
- 任务持久化到 MQ 或数据库
- 告警通知
- 根据业务降级
execute() 和 submit() 区别?
execute()
用于提交没有返回值的任务。
threadPool.execute(() -> {
System.out.println("执行任务");
});特点:
- 没有返回值
- 只能提交 Runnable
- 任务异常会直接抛出,通常会被线程的 UncaughtExceptionHandler 处理
submit()
用于提交有返回值或需要拿到执行结果的任务。
Future<Integer> future = threadPool.submit(() -> {
return 1 + 1;
});
Integer result = future.get();特点:
- 有返回值,返回 Future
- 可以提交 Runnable,也可以提交 Callable
- 异常不会直接抛出,而是封装在 Future 中
- 调用
future.get()时才会抛出ExecutionException
核心区别:execute() 只负责提交任务,没有返回值;submit() 会返回 Future,可以获取结果或异常。submit() 的异常会被封装到 Future 中,只有调用 get() 时才能感知。
线程池为什么不建议用 Executors 创建?
因为 Executors 提供的快捷方法容易隐藏风险,尤其是队列无界或线程数过大,可能导致 OOM。
1. Executors.newFixedThreadPool()
底层使用 LinkedBlockingQueue,默认容量是 Integer.MAX_VALUE。
如果任务提交速度大于消费速度,队列会无限堆积,可能导致内存溢出。
2. Executors.newSingleThreadExecutor()
也是使用无界队列。
如果任务过多,也可能 OOM。
3. Executors.newCachedThreadPool()
底层最大线程数是 Integer.MAX_VALUE。
在高并发场景下可能创建大量线程,导致系统资源耗尽。
4. Executors.newScheduledThreadPool()
最大线程数也是 Integer.MAX_VALUE。
也存在资源耗尽风险。
推荐做法
手动使用 ThreadPoolExecutor 创建线程池,明确指定:
- corePoolSize
- maximumPoolSize
- workQueue
- threadFactory
- handler
Executors 创建线程池虽然方便,但很多默认配置使用无界队列或过大的最大线程数,容易导致 OOM,所以生产环境建议手动创建 ThreadPoolExecutor,明确参数。
如何优雅关闭线程池?
线程池关闭常用两个方法:
shutdown()
threadPool.shutdown();表示不再接收新任务,但会继续执行已经提交的任务,包括队列中的任务。
shutdownNow()
threadPool.shutdownNow();尝试立即关闭线程池:
- 不再接收新任务
- 尝试中断正在执行的任务
- 返回队列中尚未执行的任务
注意:shutdownNow() 不一定能立即停止任务,因为线程中断需要任务自己响应。
优雅关闭推荐写法
public void shutdownThreadPool(ExecutorService executor) {
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("线程池未能正常关闭");
}
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}这段代码的逻辑是:
- 先调用
shutdown(),停止接收新任务 - 等待一段时间,让已提交任务执行完
- 如果超时还没结束,再调用
shutdownNow() - 如果当前线程被中断,要恢复中断标记
优雅关闭线程池一般先调用
shutdown(),然后用awaitTermination()等待任务执行完成,如果超时再调用shutdownNow(),并且在捕获InterruptedException后恢复中断状态。