你有没有遇到过这样的场景:程序里频繁创建和销毁线程,结果系统负载飙升,响应越来越慢?就像高峰期打车,每来一个乘客就叫一辆新车,司机刚接单又得返程,资源白白浪费。其实,线程也一样,反复创建销毁代价不小。这时候,线程池就派上用场了。
线程池的核心:不让线程“干完就走”
线程池的本质不是每次任务来了再开线程,而是提前准备一批“待命”的线程。这些线程不会在执行完一个任务后就退出,而是继续等待下一个任务。这种“一个线程执行多个任务”的方式,就是线程复用。
举个生活化的例子:餐厅里的服务员。如果每来一位顾客就招一个新服务员,吃完就辞退,成本太高。更聪明的做法是固定几个服务员轮班,轮流接待顾客。线程池就像这家餐厅的排班系统,服务员(线程)重复上岗,效率自然提升。
任务队列 + 空闲线程 = 高效协作
线程池内部通常包含一组线程和一个任务队列。当提交任务时,不会直接交给某个线程,而是先放入队列。空闲线程会主动从队列中“取活干”。如果没有任务,线程就阻塞等待;一旦有新任务入队,就会唤醒线程去处理。
这种设计避免了线程频繁创建和销毁的开销,也控制了并发数量,防止系统被大量线程拖垮。
代码看本质:ThreadPoolExecutor 的工作流程
以 Java 中常用的 ThreadPoolExecutor 为例,来看看线程复用是怎么实现的:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(10) // 任务队列
);
for (int i = 0; i < 8; i++) {
executor.execute(() -> {
System.out.println("任务正在执行,线程名:" + Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
这段代码创建了一个线程池,核心线程数为 2。即使提交了 8 个任务,也不会创建 8 个线程。前两个任务由核心线程处理,后续任务进入队列等待。核心线程执行完一个任务后,不会销毁,而是继续从队列取下一个任务执行,实现了真正的复用。
复用背后的“守则”
线程复用能成立,关键在于线程的生命周期被延长了。普通线程执行完 run() 方法就结束了,而线程池中的线程 run() 方法里是一个循环:
- 尝试从任务队列获取任务
- 如果拿到任务,就执行它
- 执行完回到第一步,继续取任务
- 如果超时没取到,且当前线程可回收,则退出循环,线程结束
这个循环让线程“活着等活”,而不是“干完就死”。
合理配置,才能发挥最大效果
线程池不是设了就能高枕无忧。核心线程数设得太小,并发上不去;设得太大,又可能拖累系统。建议根据实际业务类型调整:CPU 密集型任务,线程数接近 CPU 核心数;IO 密集型可以适当多一些,因为线程经常在等待读写。
用好线程池,就像管好一支高效的团队,每个人都有事做,又不至于人浮于事。理解线程复用机制,才能在开发中避开性能坑,让系统跑得更稳更快。