一、一个newFixedThreadPool差点拖垮整个系统
2021年,我们一个新人写了一段代码:
java
ExecutorService executor = Executors.newFixedThreadPool(50);
for (Order order : orders) {
executor.submit(() -> processOrder(order));
}
看起来没问题对吧?但他没考虑一点:orders有10万条,每条任务要处理3秒。
50个线程处理10万条,需要6000秒。更关键的是,每个任务还要往另一个队列里塞数据,那个队列也是无界的。
内存直接被撑爆,OOM崩溃。
从那以后,我们对线程池的使用有了严格的规范。
二、线程池核心参数
2.1 ThreadPoolExecutor七大参数
java
/**
* ThreadPoolExecutor七大参数
*/
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程空闲存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 工作队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
2.2 参数配置原则
┌─────────────────────────────────────────────────────────────────┐
│ 线程池参数配置原则 │
│ │
│ CPU密集型任务: │
│ - 核心线程数 = CPU核数 + 1 │
│ - 最大线程数 = CPU核数 + 1 │
│ - 队列:有界队列 │
│ │
│ IO密集型任务: │
│ - 核心线程数 = CPU核数 × 2 │
│ - 最大线程数 = CPU核数 × 4 │
│ - 队列:有界队列 │
│ │
│ 混合型任务: │
│ - 核心线程数 = CPU核数 × 2 │
│ - 最大线程数 = CPU核数 × 4~8 │
│ - 队列:有界队列 │
│ │
│ ⚠️ 关键原则: │
│ 1. 必须使用有界队列,防止OOM │
│ 2. 必须设置合理的拒绝策略 │
│ 3. 线程命名要可识别 │
│ │
└──────────────────────────────────────────────────────────────────┘
三、线程池配置最佳实践
3.1 基础配置
java
/**
* 线程池配置
*/
@Configuration
public class ThreadPoolConfig {
@Bean("orderProcessPool")
public ThreadPoolExecutor orderProcessPool() {
return new ThreadPoolExecutor(
10, // 核心线程数
50, // 最大线程数
60, TimeUnit.SECONDS, // 空闲存活时间
new LinkedBlockingQueue<>(1000), // 有界队列(容量1000)
new NamedThreadFactory("order-process"), // 命名线程工厂
new CustomRejectedExecutionHandler() // 自定义拒绝策略
);
}
@Bean("asyncTaskPool")
public ThreadPoolExecutor asyncTaskPool() {
return new ThreadPoolExecutor(
5,
20,
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(500),
new NamedThreadFactory("async-task"),
new ThreadPoolExecutor.CallerRunsPolicy() // 调用者执行
);
}
}
/**
* 命名线程工厂
*/
public class NamedThreadFactory implements ThreadFactory {
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
public NamedThreadFactory(String namePrefix) {
this.namePrefix = namePrefix + "-thread-";
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, namePrefix + threadNumber.getAndIncrement());
t.setDaemon(false);
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
3.2 自定义拒绝策略
java
/**
* 自定义拒绝策略:记录日志 + 降级处理
*/
@Slf4j
public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
log.error("线程池拒绝执行: pool={}, active={}, queue={}, completed={}",
executor,
executor.getActiveCount(),
executor.getQueue().size(),
executor.getCompletedTaskCount());
// 降级方案1:尝试再次入队(等待1秒)
try {
if (!executor.getQueue().offer(r, 1, TimeUnit.SECONDS)) {
log.error("再次入队失败,执行降级处理");
// 降级方案2:记录到数据库,后续重试
saveToRetryQueue(r);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("等待入队被中断");
}
}
private void saveToRetryQueue(Runnable r) {
// 将任务保存到数据库或消息队列,后续重试
}
}
3.3 线程池监控
java
/**
* 线程池监控
*/
@Component
@Slf4j
public class ThreadPoolMonitor {
@Autowired
private Map<String, ThreadPoolExecutor> threadPools;
@Autowired
private MeterRegistry meterRegistry;
/**
* 定时采集线程池指标
*/
@Scheduled(fixedRate = 5000)
public void monitor() {
threadPools.forEach((name, pool) -> {
// 核心指标
gauge(name, "active", pool.getActiveCount());
gauge(name, "pool_size", pool.getPoolSize());
gauge(name, "queue_size", pool.getQueue().size());
gauge(name, "completed", pool.getCompletedTaskCount());
gauge(name, "task_count", pool.getTaskCount());
gauge(name, "largest_pool_size", pool.getLargestPoolSize());
// 告警
if (pool.getQueue().size() > pool.getQueue().size() * 0.8) {
log.warn("线程池队列使用率超过80%: name={}, queue={}/{}",
name, pool.getQueue().size(),
((LinkedBlockingQueue<?>) pool.getQueue()).remainingCapacity() + pool.getQueue().size());
}
if (pool.getActiveCount() == pool.getMaximumPoolSize()) {
log.warn("线程池已满: name={}, active={}/{}",
name, pool.getActiveCount(), pool.getMaximumPoolSize());
}
});
}
private void gauge(String poolName, String metric, double value) {
meterRegistry.gauge("thread.pool." + metric,
Tags.of("pool", poolName), value);
}
}
四、Spring异步线程池
4.1 配置
java
/**
* Spring异步线程池配置
*/
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(500);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
log.error("异步任务执行失败: method={}, params={}",
method.getName(), Arrays.toString(params), ex);
};
}
}
/**
* 异步任务使用
*/
@Service
@Slf4j
public class AsyncTaskService {
/**
* 异步发送邮件
*/
@Async
public CompletableFuture<Void> sendEmailAsync(String to, String subject, String content) {
emailService.send(to, subject, content);
return CompletableFuture.completedFuture(null);
}
/**
* 异步处理订单
*/
@Async("orderProcessPool") // 指定线程池
public CompletableFuture<OrderResult> processOrderAsync(Order order) {
OrderResult result = processOrder(order);
return CompletableFuture.completedFuture(result);
}
}
五、踩坑实录
坑1:使用Executors创建线程池
newFixedThreadPool和newSingleThreadPool使用无界队列,任务堆积导致OOM。
解决:永远不要用Executors创建线程池,用ThreadPoolExecutor。
坑2:队列太大
队列设了10000,任务堆积在队列里,响应时间越来越长。
解决:队列容量要适中,满了就创建新线程或拒绝。
坑3:拒绝策略选错
用了AbortPolicy,任务被拒绝后直接抛异常,导致业务中断。
解决:根据业务选择合适的拒绝策略,推荐CallerRunsPolicy或自定义。
坑4:线程池没有关闭
应用关闭时线程池还在运行,任务丢失。
解决 :设置waitForTasksToCompleteOnShutdown=true,优雅关闭。
坑5:ThreadLocal泄漏
线程池中的线程是复用的,ThreadLocal的值可能残留。
解决:任务执行前后清理ThreadLocal,或使用TransmittableThreadLocal。
六、总结
线程池使用规范:
| 原则 | 说明 |
|---|---|
| 不用Executors | 手动创建ThreadPoolExecutor |
| 有界队列 | 防止OOM |
| 命名线程 | 便于排查问题 |
| 拒绝策略 | 根据业务选择 |
| 监控告警 | 及时发现问题 |
| 优雅关闭 | 防止任务丢失 |
最佳实践:
- 线程池参数可配置(通过配置中心)
- 每个业务使用独立的线程池
- 线程池要有监控和告警
- 压测验证线程池参数
- 定期Review线程池配置
血的教训:
线程池不是配好就完事了。它需要持续监控、持续调优。一个配置不当的线程池,比没有线程池更可怕。
思考题: 你的系统线程池是怎么配置的?有没有出过问题?
个人观点,仅供参考