线程
生命周期

何时我们会考虑使用线程池?
为什么用线程池?
线程的创建/销毁开销大,频繁创建线程会:
- 创建传统的线程将消耗大量系统资源(每个线程默认占用 512KB~1MB 栈空间)
- 导致频繁的上下文切换,降低 CPU 利用率
- 在高并发场景下可能因线程数失控导致 OOM
- 异步解耦:下单后异步发短信/推送通知,主流程不阻塞
- 批量并行处理:批量调用第三方 API、并行查多个数据源(配合
CompletableFuture) - IO 密集型服务:HTTP 请求处理、数据库查询,线程大部分时间在等待,可以开较多线程
- CPU 密集型计算:图像处理、报表生成,线程数建议 ≈ CPU 核心数
- 定时/周期任务:心跳检测、缓存刷新,用
ScheduledThreadPoolExecutor
参数设置:
IO 密集型:
corePoolSize = 2 × CPU核心数CPU 密集型:
corePoolSize = CPU核心数 + 1关于
Executors
Executors提供的四种标准线程池
线程池 核心线程 最大线程 队列 适用场景 newFixedThreadPool(n)n n LinkedBlockingQueue(无界)负载稳定、任务量可预估 newCachedThreadPool()0 Integer.MAX_VALUESynchronousQueue大量短时任务、突发流量 newSingleThreadExecutor()1 1 LinkedBlockingQueue(无界)需要严格串行执行 newScheduledThreadPool(n)n Integer.MAX_VALUEDelayedWorkQueue定时任务、周期任务 ⚠️ 阿里规范禁止直接用
Executors工厂方法:Fixed/Single的无界队列可能堆积大量任务导致OOM;Cached的线程数无上限同样危险。生产环境请直接用ThreadPoolExecutor。
关于 ThreadPoolExecutor
new ThreadPoolExecutor(
int corePoolSize, // 核心线程数(常驻)
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程空闲存活时间
TimeUnit unit,
BlockingQueue workQueue, // 任务队列
ThreadFactory threadFactory,
RejectedExecutionHandler handler // 拒绝策略
);
```
**执行流程:**
```
提交任务
→ 核心线程未满?→ 创建核心线程执行
→ 队列未满? → 入队等待
→ 最大线程未满?→ 创建非核心线程执行
→ 触发拒绝策略
四种拒绝策略:
| 策略 | 行为 |
|---|---|
AbortPolicy(默认) | 抛出 RejectedExecutionException |
CallerRunsPolicy | 由调用方线程直接执行,起到限流效果 |
DiscardPolicy | 静默丢弃 |
DiscardOldestPolicy | 丢弃队列头部最旧的任务 |
关于虚拟线程
创建虚拟线程
Executors.newVirtualThreadPerTaskExecutor()
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10000; i++) {
executor.submit(() -> {
// 每个任务独占一个虚拟线程
Thread.sleep(Duration.ofMillis(100));
System.out.println(Thread.currentThread());
});
}
} // try-with-resources 自动 shutdown
注意事项
① 避免池化虚拟线程
// ❌ 错误:用固定线程池包装虚拟线程,失去意义
ExecutorService wrong = Executors.newFixedThreadPool(
100, Thread.ofVirtual().factory()
);
② 避免 ThreadLocal 滥用
虚拟线程数量庞大,ThreadLocal 存储大对象会造成内存压力,考虑用 ScopedValue(JDK 21 preview)替代。
③ CPU 密集型任务仍用平台线程池
虚拟线程的优势在于 IO 等待时自动让出载体线程。纯计算任务用虚拟线程没有收益,反而增加调度开销。
④ 小心 synchronized 导致的 pinning
虚拟线程在 synchronized 块内阻塞时会**固定(pin)**载体线程,导致载体线程无法被其他虚拟线程使用。推荐改用 ReentrantLock:
// ❌ 可能 pin 住载体线程
synchronized (lock) {
doBlockingIO();
}
// ✅ 推荐
lock.lock();
try {
doBlockingIO();
} finally {
lock.unlock();
}
关于 ScheduledExecutorService
ScheduledExecutorService 核心接口
public interface ScheduledExecutorService extends ExecutorService {
// 延迟一次性执行
ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);
// 固定频率:上次【开始】时间 + period 后触发下次
ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
// 固定延迟:上次【结束】时间 + delay 后触发下次
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
}
```
---
### `AtFixedRate` vs `WithFixedDelay`
```
AtFixedRate(period = 2s,任务耗时 1s):
|--task--| wait |--task--| wait |--task--|
0 1 2 3 4 5
AtFixedRate(period = 2s,任务耗时 3s,超时):
|-----task-----|--task-----|-----task-----|
0 3 6 9
← 超时后不等待,上次结束立即触发下次 →
WithFixedDelay(delay = 2s,任务耗时 1s):
|--task--|--2s--|--task--|--2s--|--task--|
0 1 3 4 6 7
← delay 从任务结束后开始计算 →
选择原则:
- 需要严格按时间点触发(如整点报表)→
AtFixedRate - 需要保证两次任务之间有足够间隔(如轮询外部接口)→
WithFixedDelay
关键细节:异常会静默吞掉任务
这是最常见的坑:
scheduler.scheduleAtFixedRate(() -> {
// 一旦抛出未捕获异常,任务会永久停止调度
// 没有任何报错提示!
riskyOperation();
}, 0, 1, TimeUnit.MINUTES);
必须在任务内部捕获异常:
java
scheduler.scheduleAtFixedRate(() -> {
try {
riskyOperation();
} catch (Exception e) {
log.error("定时任务执行异常", e);
// 异常被消化,调度继续
}
}, 0, 1, TimeUnit.MINUTES);
取消与关闭
java
// 取消单个任务
ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(...);
future.cancel(false); // false = 不中断正在执行的任务
// true = 中断正在执行的任务
// 关闭线程池(两种方式)
scheduler.shutdown(); // 等待已提交任务执行完
scheduler.shutdownNow(); // 立即中断,返回未执行任务列表
线程数设置建议
ScheduledThreadPool 的线程数 ≠ 并发执行数,而是同时可执行的任务数。
// 如果同一时刻最多有 N 个定时任务可能并发触发
// 线程数设为 N 即可,不需要过多
Executors.newScheduledThreadPool(N);
Future 接口
Future 是一个接口。代表一个异步计算的结果,表示一个可能还没有完成的任务最终会完成的计算。
// Future 的位置在 java.util.concurrent 包
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
核心思想:
- 当你向线程池提交一个任务时,线程池返回一个
Future对象 - 这个对象就代表这个任务的未来结果
- 你可以通过这个对象查询任务状态、获取结果、取消任务
常用方法
get(5,TimeUnit.SECONEDS);阻塞当前线程,直到异步任务完成,返回任务的计算结果。
如果任务抛出异常,get()会抛出ExecutionException。
如果当前线程被中断,抛出InterruptedException。
无超时的get()操作是危险的,可能导致永久阻塞。boolean isDone();如果任务已完成,返回true(正常完成或异常或被取消)。
非阻塞式查询。boolean cancel(boolean mayInterruptIfRunning);取消任务。mayInterruptIfRunning = true:如果任务正在运行,尝试中断它1 。mayInterruptIfRunning = false:只有在任务还未开始时才取消,如果任务已开始则不取消。boolean isCancelled();
返回true如果任务被成功取消。
返回false如果任务未被取消或已完成。
- 中断真正成功的两条路:
路径1:任务在阻塞操作中。路经2:任务主动检查中断标记。(如果不满足,任务并不会被中断。) ↩︎