Java 线程池:三个经典踩坑场景与实战经验总结

作者 lucy · 2026-04-14

⚠️ 声明:本文相关内容仅供参考,实际效果因场景不同可能差异很大,请结合自身情况判断,谨慎参考。

线程池是 Java 并发编程的基础设施,用得好可以扛住高并发,用不好就是生产事故的重灾区。这几年我在线上踩过几个线程池相关的坑,记录下来供参考。

场景一:为什么你的线程池总是不复用?

项目里有一段定时任务,用 Executors.newFixedThreadPool(10) 提交了 1000 个任务,监控显示线程数从 10 跳到了 1000——线程池根本没有起到复用的作用。

// 错误用法:每次循环 new 一个任务丢进去
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
    final int taskId = i;
    pool.submit(() -> {
        doSomething(taskId);
    });
}

// 实际情况:1000个任务同时抢10个线程,排队堆积
// 线程复用了吗?复用了
// 但提交速度 >> 处理速度,队列积压严重

问题不在线程池,在于 提交方式和线程池大小的匹配关系

解决方案(仅供参考)

// 正确做法1:调整线程池大小 + 限流
ExecutorService pool = new ThreadPoolExecutor(
    10,          // corePoolSize:核心线程数
    20,          // maximumPoolSize:允许膨胀到20
    60L,         // keepAliveTime:空闲线程存活时间
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(200),  // 有界队列,控制积压上限
    new ThreadPoolExecutor.CallerRunsPolicy()  // 队列满时由调用方执行,反压
);

// 正确做法2:用 CompletableFuture 批量提交,控制并发数
List<CompletableFuture<?>> futures = IntStream.range(0, 1000)
    .mapToObj(i -> CompletableFuture.runAsync(() -> doSomething(i), pool))
    .collect(Collectors.toList());
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

场景二:核心线程数设错了,系统资源被打满

CPU 密集型任务(加密、压缩、计算)和 IO 密集型任务(数据库调用、HTTP 请求)需要的线程数完全不一样,但很多人用同一套公式:

// 这个公式只对 IO 密集型有效
// 线程数 = CPU核心数 × (1 + IO等待时间 / CPU计算时间)
// Runtime.getRuntime().availableProcessors() 获取核心数

// 如果是4核CPU,IO任务等待时间/CPU计算时间=3:1
// 最佳线程数 = 4 × (1 + 3) = 16

// 但如果你的任务是纯计算(加密、压缩):
// 线程数 = CPU核心数 = 4  // 设多了反而切换开销大
// 甚至可以设为 CPU核心数 + 1 留一点余量

实际项目中,常见问题是把数据库查询这种 IO 密集型任务的线程数设成和 CPU 密集型一样,导致线程大量等待 DB 连接,CPU 却空闲。

解决方案(仅供参考)

// 根据任务类型选择线程池
public class ThreadPoolFactory {

    // CPU 密集型:线程数 = CPU核心数 + 1
    public static ExecutorService cpuPool() {
        int core = Runtime.getRuntime().availableProcessors();
        return new ThreadPoolExecutor(
            core, core,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<>(1000),
            new DefaultThreadFactory("cpu-pool"),
            new CallerRunsPolicy()
        );
    }

    // IO 密集型:线程数 = CPU核心数 × 2,或按公式计算
    public static ExecutorService ioPool() {
        int core = Runtime.getRuntime().availableProcessors();
        int poolSize = core * 2;  // 或 × (1 + 3) 根据实际 IO 比例调整
        return new ThreadPoolExecutor(
            poolSize, poolSize * 2,
            60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(500),
            new DefaultThreadFactory("io-pool"),
            new CallerRunsPolicy()
        );
    }
}

场景三:线程池的拒绝策略没配好,任务直接丢了

队列满了,新任务进来,如果拒绝策略是默认的 AbortPolicy,任务直接抛异常丢失,用户不知道,排查半天发现不了。

// AbortPolicy 默认行为:队列满时抛 RejectedExecutionException
// 生产环境如果不想丢任务,用 CallerRunsPolicy
// 它会把任务交回给调用方线程执行,既不丢任务,又能通过调用方的阻塞来限流

new ThreadPoolExecutor.CallerRunsPolicy()

// 更好的方案:自定义策略 + 告警
new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 发送告警
        Metrics.counter("threadpool.rejected").increment();
        // 降级处理:存入本地队列稍后重试
        fallbackQueue.offer(r, 5, TimeUnit.SECONDS);
    }
}

经验总结

  1. 永远不要用 Executors 的快捷方法创建线程池。 newFixedThreadPool 和 newCachedThreadPool 的队列是 Integer.MAX_VALUE 的无界队列,内存溢出的经典原因之一。用 ThreadPoolExecutor 显式创建,控制队列大小。

  2. 核心线程数不是越大越好。 IO 密集型可以多设,CPU 密集型一定要压住 CPU 核心数。实测比公式更重要的是压测:在测试环境跑不同线程数,找到拐点。

  3. 拒绝策略是最后的安全网。 建议用 CallerRunsPolicy 再配合监控告警,不要静默丢弃任务。

  4. 监控是线程池管理的必备项。 以下指标一定要盯紧:活跃线程数、队列大小、已完成任务数、拒绝次数、死线程数。

  5. 线程池里的线程要设明名。 用 ThreadFactory 给线程加上业务前缀,线上出问题排查线程栈时,一眼就能定位是哪个池。

// 命名线程工厂,方便排查
ThreadFactory factory = new ThreadFactory() {
    private final AtomicInteger counter = new AtomicInteger(1);
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, "task-pool-" + counter.getAndIncrement());
        t.setDaemon(false);
        return t;
    }
};

⚠️ 再声明:线程池调优需要结合具体业务压测结果,核心线程数和队列大小的组合直接影响系统吞吐和内存占用,建议在测试环境充分验证后再上生产。

有问题欢迎留言交流 🚀

发表评论

苏ICP备18039580号-2