【架构实战】线程池设计:高并发系统的资源管理艺术

一、一个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创建线程池

newFixedThreadPoolnewSingleThreadPool使用无界队列,任务堆积导致OOM。

解决:永远不要用Executors创建线程池,用ThreadPoolExecutor。

坑2:队列太大

队列设了10000,任务堆积在队列里,响应时间越来越长。

解决:队列容量要适中,满了就创建新线程或拒绝。

坑3:拒绝策略选错

用了AbortPolicy,任务被拒绝后直接抛异常,导致业务中断。

解决:根据业务选择合适的拒绝策略,推荐CallerRunsPolicy或自定义。

坑4:线程池没有关闭

应用关闭时线程池还在运行,任务丢失。

解决 :设置waitForTasksToCompleteOnShutdown=true,优雅关闭。

坑5:ThreadLocal泄漏

线程池中的线程是复用的,ThreadLocal的值可能残留。

解决:任务执行前后清理ThreadLocal,或使用TransmittableThreadLocal。


六、总结

线程池使用规范:

原则 说明
不用Executors 手动创建ThreadPoolExecutor
有界队列 防止OOM
命名线程 便于排查问题
拒绝策略 根据业务选择
监控告警 及时发现问题
优雅关闭 防止任务丢失

最佳实践:

  1. 线程池参数可配置(通过配置中心)
  2. 每个业务使用独立的线程池
  3. 线程池要有监控和告警
  4. 压测验证线程池参数
  5. 定期Review线程池配置

血的教训:

线程池不是配好就完事了。它需要持续监控、持续调优。一个配置不当的线程池,比没有线程池更可怕。

思考题: 你的系统线程池是怎么配置的?有没有出过问题?


个人观点,仅供参考

相关推荐
笨鸟飞不快37 分钟前
从 MVC 到 DDD:一次真实的渐进式迁移实录
后端·架构
这个DBA有点耶18 小时前
GROUP BY优化全解:如何写出既不丢数据又飞快的分组查询
数据库·mysql·架构
锋行天下18 小时前
我试图优化 Vite 的拆包,结果首屏慢了 10 倍
前端·vue.js·架构
小鼻子的猫1 天前
独立开发 30 天:2.5 万行代码,23 个 Bug,5 次重构——一个 AI 社区的诞生
架构
咖啡八杯1 天前
GoF设计模式——命令模式
java·设计模式·架构
candyTong1 天前
阿里开源 AI Code Review 工具:ocr review 的执行链路解析
javascript·后端·架构
doiito2 天前
【Agent Harness】TPS的“自工程完结”教会了我一件事:别把Bug留给下一道工序
架构·rust
烬羽2 天前
中英文 token 数量差一倍?两段 JS 代码搞懂 LLM 底层是怎么"读"文字的
javascript·程序员·架构
白鲸开源2 天前
一文读懂DolphinScheduler插件机制:如何轻松扩展任务类型与数据源
java·架构·github