每个任务都创建一个线程,会反复产生创建和销毁的开销。线程池把工作线程留下来,让它们不断取任务执行。理解线程池,先看任务提交后去了哪里。
核心线程、队列和最大线程
ThreadPoolExecutor 收到任务后,主要按下面的顺序处理:
- 工作线程数小于 corePoolSize,尝试创建线程执行任务。
- 达到核心数量后,尝试把任务放进队列。
- 队列放不进去,再尝试增加线程,直到 maximumPoolSize。
- 仍然无法接收,交给拒绝策略。
所以不是“先把线程开到 maximumPoolSize,再往队列里放”。如果使用容量很大的队列,任务通常会先在队列里等,最大线程数未必有机会发挥作用。
看一下实际分配
下面先让任务等待一个门闩,避免它们太快结束,方便观察线程和队列:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class PoolFlowDemo {
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 2, 30,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(1));
CountDownLatch release = new CountDownLatch(1);
Runnable task = () -> {
try {
release.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
try {
pool.execute(task);
pool.execute(task);
pool.execute(task);
System.out.println("线程数=" + pool.getPoolSize());
System.out.println("排队数=" + pool.getQueue().size());
try {
pool.execute(task);
} catch (RejectedExecutionException e) {
System.out.println("第四个任务被拒绝");
}
} finally {
release.countDown();
pool.shutdown();
}
if (!pool.awaitTermination(5, TimeUnit.SECONDS)) pool.shutdownNow();
}
}线程数为 2,排队数为 1,第四个任务被拒绝。第一个任务创建核心线程,第二个入队,第三个因为队列已满而创建第二个线程。
Worker 做什么
JDK 8 用 Worker 管理工作线程。线程启动后进入 runWorker(),先执行创建时携带的第一个任务,然后通过 getTask() 从队列继续取。
超过核心数量的空闲线程通常使用带超时的 poll 获取任务;等待超时且符合退出条件时,工作线程退出。允许核心线程超时后,核心部分也可以参与回收。因此“核心线程”不是给某条 Thread 永久贴上的身份标签。
execute 在任务成功入队后还会重新检查线程池状态。提交和关闭可能同时发生,入队成功不能直接说明线程池仍接受它;此外,如果此时没有工作线程,也需要补建线程处理队列。
shutdown 不等于等完
shutdown() 停止接受新任务,继续处理已提交任务,但方法本身不会等所有任务结束。需要等待时使用 awaitTermination()。
shutdownNow() 尝试中断正在执行的任务,并返回尚未开始的排队任务,也不能保证忽略中断的任务立刻停止。线程池能管理任务的执行过程,任务自身仍需配合取消协议。