Java 线程池详解
1. 一句话总结:为什么要用线程池?
线程是稀缺资源,如果无限制地创建,不仅会消耗系统内存,还会导致 CPU 过度切换。
线程池的核心作用:
- 降低资源消耗:线程可复用,减少频繁创建/销毁。
- 提高响应速度:任务来了直接复用线程执行。
- 提高可管理性:统一控制并发度、队列容量、拒绝策略,便于监控和限流。
2. 生活化比喻:线程池就是一个"银行网点"
corePoolSize:正式窗口(常驻员工)maximumPoolSize:最多能开的窗口数(正式 + 临时)workQueue:大厅等候座位keepAliveTime:临时窗口空闲多久撤掉handler:保安(满了怎么处理新客人)
3. 核心参数(ThreadPoolExecutor 7 大金刚)
| 参数 | 含义 | 通俗解释 |
|---|---|---|
corePoolSize |
核心线程数 | 常驻窗口数(默认不回收) |
maximumPoolSize |
最大线程数 | 最大窗口数(含临时加班窗口) |
keepAliveTime |
存活时间 | 临时窗口空闲多久就撤掉 |
unit |
时间单位 | 秒/毫秒/分钟等 |
workQueue |
任务队列 | 排队区的座位(阻塞队列) |
threadFactory |
线程工厂 | 线程怎么创建(命名、优先级、是否守护线程) |
handler |
拒绝策略 | 忙不过来时的兜底策略 |
4. 任务提交后的执行流程(面试必考)
新任务进来后,线程池基本按这个顺序处理(记住这 4 步就够回答 80% 问题):
4.1 流程图解
text
[ 任务提交 ]
|
v
+-----------------------+ 是 +----------------------+
| 运行线程 < coreSize? |------>| 创建【核心线程】执行 |
+-----------------------+ +----------------------+
| 否
v
+-----------------------+ 是 +----------------------+
| workQueue 未满? |------>| 任务进入【队列】 |
+-----------------------+ +----------------------+
| 否
v
+-----------------------+ 是 +----------------------+
| 运行线程 < maxSize? |------>| 创建【非核心线程】执行 |
+-----------------------+ +----------------------+
| 否
v
+-----------------------+
| 触发【拒绝策略】 | <-- 保安拦截 (Abort/CallerRuns/Discard...)
+-----------------------+
4.2 详细步骤说明(重点:核心线程优先于队列)
- 第一步:判断核心线程池 。如果当前运行的线程数 <
corePoolSize,则直接创建核心线程执行任务。(注意:即使此时队列没满,也会优先创建核心线程) - 第二步:判断任务队列 。如果核心线程已满,则尝试将任务放入
workQueue排队。 - 第三步:判断最大线程池 。如果队列也满了,且当前运行线程数 <
maximumPoolSize,则创建非核心线程(临时工)执行任务。 - 第四步:触发拒绝策略 。如果线程数已达到
maximumPoolSize且队列已满,则执行handler拒绝任务。
面试官常问:为什么不先放队列,再开核心线程?
底层设计逻辑 :核心线程被视为"常驻效率单元"。为了保证任务能以最快速度响应,Java 线程池的设计是先尽量让核心线程跑起来,只有核心线程都忙不过来了,才让任务去排队。如果先排队,任务就必须等待线程调度,响应速度会变慢。
5. 四种常见拒绝策略
| 策略 | 行为 | 适用场景 |
|---|---|---|
AbortPolicy(默认) |
直接抛 RejectedExecutionException |
必须快速失败、报警 |
CallerRunsPolicy |
调用者线程自己执行 | 不能丢任务,允许变慢(削峰) |
DiscardPolicy |
直接丢弃,不抛异常 | 可容忍丢弃(如统计、非关键日志) |
DiscardOldestPolicy |
丢最旧的任务,尝试提交新任务 | 更关注最新任务 |
6. 创建线程池的两种方式:Executors vs ThreadPoolExecutor(重点对比)
线程池的创建主要有两种方式:
- 通过
Executors工厂类快速创建(写起来省事,但隐藏默认参数) - 通过
ThreadPoolExecutor手动配置(参数可控,生产更推荐)
6.1 Executors:快速创建(适合学习/小工具/对稳定性要求不高的场景)
常见工厂方法:
Executors.newFixedThreadPool(n):固定大小线程池Executors.newSingleThreadExecutor():单线程池(保证顺序执行)Executors.newCachedThreadPool():缓存线程池(任务来了就扩容)Executors.newScheduledThreadPool(n):定时/周期任务线程池
优点:
- 一行代码就能用,上手快。
缺点(面试必背):
- 很多工厂方法的默认参数是不受控 的,容易在高并发时把系统拖死:
newFixedThreadPool/newSingleThreadExecutor:默认使用无界队列 (任务无限堆积)→ 可能 OOM。newCachedThreadPool:最大线程数几乎无限(任务暴增会疯狂建线程)→ 可能 CPU 飙高 / OOM。
6.2 ThreadPoolExecutor:手动配置(生产推荐)
你需要显式指定核心参数:核心线程数、最大线程数、队列容量、拒绝策略等。
优点:
- 可控:能明确限制"最多开多少线程""最多排多少队",更稳定。
- 可观测:可配合线程命名、埋点、监控来定位问题。
- 可降级:可以按业务选择拒绝策略(抛异常/调用者执行/丢弃等)。
缺点:
- 写起来比
Executors繁琐,需要你理解参数含义。
6.3 两者对比表(建议面试直接念)
| 维度 | Executors 工厂类 |
ThreadPoolExecutor 手动配置 |
|---|---|---|
| 上手成本 | 低(创建快) | 中(要理解 7 个参数) |
| 参数可控性 | 低(默认值隐藏) | 高(每个参数可控) |
| 生产安全性 | 低(常见 OOM/CPU 飙高隐患) | 高(能限制队列/线程数) |
| 监控/排障 | 一般 | 更好(可自定义线程名、策略) |
| 推荐程度 | 学习/小工具可用 | 生产强烈推荐 |
一句话结论:
Executors适合"快速写个能跑的 demo"。ThreadPoolExecutor适合"线上要扛住流量、要稳定"。
7. 参数怎么设置更合理?(经验法则)
- CPU 密集型:
CPU 核心数或CPU 核心数 + 1 - IO 密集型:
CPU 核心数 * 2(更精确需要压测/结合等待时间)
更多代码示例(面试/实战常用)
覆盖:线程命名、不同队列、拒绝策略、submit/Future、定时任务、优雅关闭、异常处理、beforeExecute/afterExecute 钩子、并发工具配合。
示例 1:最常用的 ThreadPoolExecutor(有界队列 + 命名线程 + CallerRuns)
java
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class Demo1_Basic {
public static void main(String[] args) {
AtomicInteger idx = new AtomicInteger(1);
ThreadFactory tf = r -> {
Thread t = new Thread(r);
t.setName("biz-pool-" + idx.getAndIncrement());
t.setDaemon(false);
return t;
};
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2,
5,
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10),
tf,
new ThreadPoolExecutor.CallerRunsPolicy()
);
for (int i = 0; i < 30; i++) {
final int taskId = i;
pool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " handling task " + taskId);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
pool.shutdown();
}
}
示例 2:对比三种常见队列
2.1 ArrayBlockingQueue(数组有界队列)
java
BlockingQueue<Runnable> q = new ArrayBlockingQueue<>(100);
2.2 LinkedBlockingQueue(链表队列,可有界/无界)
java
BlockingQueue<Runnable> q = new LinkedBlockingQueue<>(1000); // 建议显式给容量
2.3 SynchronousQueue(直接交接,不存储元素)
java
BlockingQueue<Runnable> q = new SynchronousQueue<>();
示例 3:快速演示 4 种拒绝策略的效果
java
import java.util.concurrent.*;
public class Demo3_RejectedHandlers {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
1,
1,
0,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy() // 换成 CallerRuns/Discard/DiscardOldest 观察不同效果
);
for (int i = 0; i < 10; i++) {
final int taskId = i;
try {
pool.execute(() -> {
System.out.println("run " + taskId);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
} catch (RejectedExecutionException ex) {
System.out.println("rejected: " + taskId + ", reason=" + ex);
}
}
pool.shutdown();
}
}
示例 4:execute() vs submit() + Future 获取结果/异常
java
import java.util.concurrent.*;
public class Demo4_SubmitFuture {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.execute(() -> {
throw new RuntimeException("boom from execute");
});
Future<Integer> f = pool.submit(() -> {
throw new IllegalStateException("boom from submit");
});
try {
System.out.println("result=" + f.get());
} catch (Exception e) {
System.out.println("caught from future: " + e);
}
pool.shutdown();
}
}
示例 5:Callable 批量执行 invokeAll
java
import java.util.*;
import java.util.concurrent.*;
public class Demo5_InvokeAll {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(4);
List<Callable<Integer>> tasks = new ArrayList<>();
for (int i = 0; i < 10; i++) {
final int x = i;
tasks.add(() -> x * x);
}
List<Future<Integer>> futures = pool.invokeAll(tasks);
for (Future<Integer> f : futures) {
System.out.println(f.get());
}
pool.shutdown();
}
}
示例 6:定时线程池(ScheduledExecutorService)
java
import java.util.concurrent.*;
public class Demo6_Scheduled {
public static void main(String[] args) throws Exception {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
scheduler.schedule(() -> System.out.println("delay once"), 1, TimeUnit.SECONDS);
scheduler.scheduleAtFixedRate(() -> System.out.println("fixedRate"), 0, 2, TimeUnit.SECONDS);
scheduler.scheduleWithFixedDelay(() -> {
System.out.println("fixedDelay start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, 0, 2, TimeUnit.SECONDS);
Thread.sleep(7000);
scheduler.shutdown();
}
}
示例 7:优雅关闭(shutdown + awaitTermination + shutdownNow)
java
import java.util.concurrent.*;
public class Demo7_GracefulShutdown {
public static void main(String[] args) throws Exception {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100)
);
for (int i = 0; i < 20; i++) {
pool.execute(() -> {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
pool.shutdown();
if (!pool.awaitTermination(2, TimeUnit.SECONDS)) {
pool.shutdownNow();
}
System.out.println("terminated=" + pool.isTerminated());
}
}
示例 8:扩展 ThreadPoolExecutor(beforeExecute/afterExecute 统计耗时)
java
import java.util.concurrent.*;
public class Demo8_Hooks {
static class TimingThreadPool extends ThreadPoolExecutor {
public TimingThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
private final ThreadLocal<Long> startTime = new ThreadLocal<>();
@Override
protected void beforeExecute(Thread t, Runnable r) {
startTime.set(System.nanoTime());
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
long costMs = (System.nanoTime() - startTime.get()) / 1_000_000;
System.out.println("task cost: " + costMs + "ms" + (t != null ? ", ex=" + t : ""));
}
@Override
protected void terminated() {
System.out.println("pool terminated");
}
}
public static void main(String[] args) {
TimingThreadPool pool = new TimingThreadPool(2, 4, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
pool.execute(() -> {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
pool.shutdown();
}
}
示例 9:配合 CountDownLatch 等待一批任务完成
java
import java.util.concurrent.*;
public class Demo9_CountDownLatch {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(4);
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
pool.execute(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
});
}
latch.await();
System.out.println("all done");
pool.shutdown();
}
}
8.(回到面试)常见追问要点
-
线程池里的线程异常了会怎样?
- 任务抛出运行时异常,执行该任务的线程可能终止;线程池会按需补充新线程以维持池规模(细节与实现/配置有关)。
-
submit()和execute()区别?execute():不返回结果;异常通常直接打印到工作线程。submit():返回Future;异常会封装进Future.get()。
-
Runnable与Callable区别?Runnable无返回值。Callable有返回值且可抛异常,常配合Future使用。