2. 线程池应用
概念
频繁创建线程会占用栈内存,也会让调度开销压过任务本身。线程池事先准备若干工人线程,任务放进队列,空闲工人取走执行。 池子要限制同时运行的线程数和排队长度。队列满了必须有明确策略:拒绝、由提交者自己跑,或阻塞提交方。任务全部提交后要 shutdown,并等待已提交任务结束,否则进程可能停不下来。
- 核心参数:核心线程数、最大线程数、空闲回收时间、工作队列、拒绝策略。
- Java 常用 ThreadPoolExecutor;不要在生产代码里使用无界的 Executors.newCachedThreadPool 承接外部请求。
- Python 用 concurrent.futures.ThreadPoolExecutor。I/O 型任务适合线程池,CPU 密集计算更适合进程池。
- submit 得到的是未来结果。调用方可以用 Future.get / future.result 取回返回值或异常。
算法流程
下面的 UML 图说明 Java 例程和 Python 例程共同实现的流程。实线箭头表示调用或发送,虚线箭头表示返回或唤起。
提交任务(UML 时序图)
sequenceDiagram
participant Caller as 提交线程
participant Pool as 线程池
participant Queue as 有界队列
participant Worker as 工人线程
Caller->>Pool: submit(task)
alt 有空闲核心线程
Pool->>Worker: 立即执行
else 核心线程都忙且队列未满
Pool->>Queue: 任务入队
Worker->>Queue: 空闲后取走执行
else 队列已满且未到最大线程数
Pool->>Worker: 创建非核心线程执行
else 队列已满且线程数已满
Pool->>Caller: 拒绝策略,提交者自己执行
end
Caller->>Pool: shutdown,不再接收新任务
Pool-->>Caller: awaitTermination,等待已提交任务结束
Java 例程
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class PoolDemo {
public static void main(String[] args) throws Exception {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, // 核心线程:平时保留 2 个工人
4, // 最大线程:忙时最多再扩到 4 个
30, TimeUnit.SECONDS, // 超出核心数的空闲线程,30 秒后回收
new ArrayBlockingQueue<Runnable>(8), // 有界队列,最多再排 8 个任务
new ThreadPoolExecutor.CallerRunsPolicy()); // 队列也满了,就由提交任务的线程自己执行
for (int i = 0; i < 6; i++) {
final int n = i; // 匿名类要捕获的变量必须是 final
pool.submit(new Runnable() { // submit 只是入队,不表示已经开始跑
public void run() {
System.out.println(Thread.currentThread().getName() + " task " + n);
}
});
}
pool.shutdown(); // 不再接收新任务,已提交的还会跑完
pool.awaitTermination(5, TimeUnit.SECONDS); // 主线程最多再等 5 秒
}
}
Python 例程
from concurrent.futures import ThreadPoolExecutor
def work(n):
return n * n # 线程池里的任务可以有返回值
# with 结束时会 shutdown(wait=True),等已提交任务完成再退出
with ThreadPoolExecutor(max_workers=4, thread_name_prefix="worker") as pool:
# submit 立即返回 Future,任务在池内线程中执行
futures = [pool.submit(work, i) for i in range(6)]
for fut in futures:
print(fut.result()) # 任务没完就阻塞在这里;任务抛异常也会从这里重新抛出