java的线程池
Java 中的线程池(Thread Pool)是多线程编程中极其重要的概念。它可以帮助我们管理和复用线程,避免频繁创建和销毁线程带来的性能开销。
下面我将从基本概念、核心参数、工作原理、常用类型、拒绝策略、以及最佳实践这几个方面,为你系统地介绍 Java 线程池。
一、 为什么使用线程池?(优势)
- 降低资源消耗:通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
二、 线程池的核心实现:ThreadPoolExecutor
Java 中的线程池核心实现类是 java.util.concurrent.ThreadPoolExecutor。它最长的一个构造方法包含 7 个核心参数(面试必考):
java
public ThreadPoolExecutor(
int corePoolSize, // 1. 核心线程数
int maximumPoolSize, // 2. 最大线程数
long keepAliveTime, // 3. 空闲线程存活时间
TimeUnit unit, // 4. 时间单位
BlockingQueue<Runnable> workQueue, // 5. 任务队列
ThreadFactory threadFactory, // 6. 线程工厂
RejectedExecutionHandler handler // 7. 拒绝策略
)
7 个参数详解:
corePoolSize(核心线程数):线程池中常驻的线程数量。即使这些线程处于空闲状态,也不会被销毁(除非设置了allowCoreThreadTimeOut)。maximumPoolSize(最大线程数):线程池允许创建的最大线程数量。当任务队列满了之后,线程池会继续创建线程,直到达到这个值。keepAliveTime(空闲线程存活时间):当线程数大于核心线程数时,多余的空闲线程存活的时间。unit(时间单位):keepAliveTime的时间单位(如TimeUnit.SECONDS)。workQueue(任务队列):用于存放等待执行的任务的阻塞队列。常见的有:LinkedBlockingQueue(无界/有界链表队列)ArrayBlockingQueue(有界数组队列)SynchronousQueue(不存储元素的阻塞队列)
threadFactory(线程工厂):用于创建新线程,可以给线程起一个有意义的名字,方便排查问题。handler(拒绝策略):当线程池和队列都满了,新提交的任务该如何处理。
三、 线程池的工作原理(工作流程)
当一个新任务被提交到线程池时,处理流程如下:
- 核心线程未满:如果当前运行的线程数小于
corePoolSize,则直接创建新线程(核心线程)来执行任务。 - 队列未满:如果当前运行的线程数达到
corePoolSize,新任务会被放入任务队列workQueue中等待。 - 最大线程未满:如果任务队列已满,且当前运行的线程数小于
maximumPoolSize,则创建非核心线程来执行任务。 - 触发拒绝策略:如果队列已满,且运行的线程数已达到
maximumPoolSize,则触发RejectedExecutionHandler拒绝策略。
比喻记忆法:
线程池就像一家外包公司。
corePoolSize是正式员工。workQueue是需求积压池(任务队列)。maximumPoolSize是正式员工 + 临时工(非核心线程)的总和。- 当正式员工忙不过来,需求先放积压池;积压池满了,公司赶紧招临时工;临时工也招满了,再来新需求,只能拒绝(拒绝策略)。
四、 四种常见的拒绝策略
当线程池无力处理新任务时,Java 提供了 4 种内置策略:
AbortPolicy(默认):直接抛出RejectedExecutionException异常,阻止系统正常运行。CallerRunsPolicy:“调用者运行”。哪条线程提交的任务,就由哪条线程来执行这个任务(通常是主线程),这会降低新任务的提交速度。DiscardOldestPolicy:丢弃队列中最老的一个任务(最先进入队列的任务),然后尝试重新提交当前任务。DiscardPolicy:直接丢弃新任务,不予任何处理也不抛出异常。
五、 常见的四种内置线程池(通过 Executors 创建)
Java 在 Executors 工具类中提供了几种快速创建线程池的方法,但不推荐在生产环境中使用(原因见后文):
CachedThreadPool(可缓存线程池)- 特点:核心线程数为0,最大线程数为
Integer.MAX_VALUE。来一个任务就建一个线程。 - 风险:可能导致创建大量线程,导致 OOM(内存溢出)。
- 特点:核心线程数为0,最大线程数为
FixedThreadPool(定长线程池)- 特点:核心线程数等于最大线程数。
- 风险:使用的是无界队列
LinkedBlockingQueue,任务堆积过多会导致 OOM。
SingleThreadExecutor(单线程池)- 特点:只有一个核心线程。
- 风险:同样使用无界队列,存在 OOM 风险。
ScheduledThreadPool(定时线程池)- 特点:支持定时及周期性任务执行。
⚠️ 阿里《Java开发手册》强制规定:
线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
并且,线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
六、 线程池的最佳实践与配置
1. 如何合理配置线程池大小?
线程池大小的设置取决于任务的类型:
- CPU 密集型任务(如:加密、计算、视频解码):
- 线程主要消耗 CPU 资源,线程数不宜过多。
- 推荐:CPU 核心数 + 1(
N_cpu + 1)。多出的一个线程是为了防止线程偶发性暂停导致 CPU 空闲。
- I/O 密集型任务(如:数据库读写、网络请求、文件读写):
- 线程大部分时间在等待 I/O 操作,CPU 比较空闲。
- 推荐:2 * CPU 核心数(
2 * N_cpu),或者使用公式:CPU 核心数 * (1 + 线程等待时间 / 线程计算时间)。
2. 代码示例(推荐的创建方式)
java
import java.util.concurrent.*;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 获取 CPU 核心数
int cpuCores = Runtime.getRuntime().availableProcessors();
// 手动创建线程池
ExecutorService threadPool = new ThreadPoolExecutor(
cpuCores, // 核心线程数
cpuCores * 2, // 最大线程数
60L, // 空闲存活时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(500),// 有界队列,防止 OOM
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:调用者运行
);
// 提交任务
for (int i = 0; i < 10; i++) {
final int index = i;
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 正在执行任务 " + index);
});
}
// 关闭线程池(优雅关闭)
threadPool.shutdown();
}
}
3. 提交任务的两种方式:execute() vs submit()
execute():用于提交不需要返回值的任务。javathreadPool.execute(new Runnable() { ... });submit():用于提交需要返回值的任务。它会返回一个Future对象,可以通过future.get()获取执行结果(会阻塞直到任务完成)。javaFuture<String> future = threadPool.submit(new Callable<String>() { ... }); String result = future.get();
总结
掌握 Java 线程池的关键在于理解 ThreadPoolExecutor 的 7 个参数以及任务排队和执行的流程。在实际开发中,务必手动创建 ThreadPoolExecutor 并指定有界队列,以确保系统的安全与稳定。