跳转至

Java 线程池详解

Java 线程池的本质:提前创建并复用一组线程,用队列缓存任务,用一套“线程数 + 队列 + 拒绝策略”的规则来管理任务提交与执行。

为什么要用线程池

  • 降低线程创建/销毁开销:线程是重量级资源,频繁 new Thread 成本高,线程池通过复用已有线程减少开销。
  • 提高响应速度:任务来了直接丢给已有线程,无需等待创建线程。
  • 可控的资源管理:统一控制并发线程数、队列大小和拒绝策略,避免“线程无限建、资源打满”。
  • 便于监控与调优:线程池暴露活跃线程数、队列长度、已完成任务数等指标,方便定位性能瓶颈。

线程池核心参数(ThreadPoolExecutor)

常用构造函数:

public ThreadPoolExecutor(
        int corePoolSize,          // 核心线程数
        int maximumPoolSize,       // 最大线程数
        long keepAliveTime,        // 非核心线程空闲存活时间
        TimeUnit unit,             // keepAliveTime 时间单位
        BlockingQueue<Runnable> workQueue, // 任务队列
        ThreadFactory threadFactory,       // 线程工厂
        RejectedExecutionHandler handler   // 拒绝策略
)

关键参数含义(记住这几个就够用):

  • corePoolSize:核心线程数,线程池“常驻工人”数量,正常情况下优先用核心线程执行任务。
  • maximumPoolSize:线程池允许的最大线程数 = 核心线程 + 非核心线程上限。
  • keepAliveTime + unit:非核心线程空闲超过这个时间就会被回收;有配置时也可让核心线程参与回收。
  • workQueue:保存等待执行任务的阻塞队列(如 ArrayBlockingQueue、LinkedBlockingQueue 等)。
  • threadFactory:创建线程的工厂,常用来给线程命名、设置守护线程、统一异常处理。
  • handler:拒绝策略,在线程池和队列都满了之后决定如何处理新任务(丢弃、抛异常、调用方执行等)。

推荐做法:生产环境使用 ThreadPoolExecutor 构造函数手动创建线程池,不直接用 Executors 提供的快捷方法(容易导致队列无界、OOM 等隐患)。

线程池的工作流程(任务提交流程)

线程池内部就是一个“生产者-消费者 + 线程复用”模型:外部提交任务,线程池判断是交给线程执行、入队还是拒绝。

当调用 execute(Runnable command) 时,大致流程是:

  1. 如果当前运行线程数 < corePoolSize:
    • 直接创建新的核心线程来执行任务,不进队列。
  2. 否则,如果线程数 ≥ corePoolSize:
    • 尝试把任务放入 workQueue 等待执行(如果队列没满)。
  3. 如果队列满了:
    • 且当前线程数 < maximumPoolSize:创建非核心线程执行任务。
  4. 如果队列满 & 线程数 ≥ maximumPoolSize:
    • 触发拒绝策略 handler(抛异常/丢弃任务/调用方线程执行等)。

示例图景:

  • corePoolSize=2,maximumPoolSize=4,队列容量=10。
  • 前 2 个任务来:立刻开启 2 个核心线程执行。
  • 第 3~12 个任务:进入队列等待。
  • 第 13、14 个任务:创建非核心线程到 4 个。
  • 第 15 个任务:线程池+队列满,触发拒绝策略。

常见线程池类型(Executors 工具类)

Executors 只是帮你包装不同参数组合,本质还是 ThreadPoolExecutor。

线程池类型 创建方式 特点与风险简述
固定大小线程池 Executors.newFixedThreadPool(n) 核心线程 = 最大线程 = n,使用无界队列,任务过多易 OOM。javaguide+1
缓存线程池 Executors.newCachedThreadPool() 线程数可扩到 Integer.MAX_VALUE,短任务多时适用,极端下也易撑爆。javaguide+1
单线程线程池 Executors.newSingleThreadExecutor() 永远一个线程,串行执行任务,适合顺序任务但仍有无界队列风险。javaguide+1
定时任务线程池 Executors.newScheduledThreadPool(n) 支持延时/周期任务,底层是 ScheduledThreadPoolExecutor。javaguide​

生产环境尽量不用上面这些直接工厂方法,而是自己 new ThreadPoolExecutor,设置有界队列并明确拒绝策略。

线程池核心参数如何配置(经验规则)

常用经验规则(实际要结合压测):

  • CPU 密集型任务(主要是计算):
    • 线程数 ≈ CPU 核心数(或略多一点,如 N 或 N+1)。
  • I/O 密集型任务(大量网络/磁盘等待):
    • 线程数 ≈ N × M,N 为 CPU 核心数,M 通常取 2~4,根据“等待时间/计算时间”比例调优。
  • 更严谨的公式:
    • 最佳线程数 ≈ N × (1 + WT / ST),N 为核心数,WT 是等待时间,ST 是计算时间。 ​ 典型 Spring 配置示例(伪代码):
@Configuration
@EnableAsync
public class ThreadPoolExecutorConfig {

    @Bean("bizThreadPool")
    public Executor threadPoolExecutor() {
        int processors = Runtime.getRuntime().availableProcessors();

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                processors,                 // corePoolSize
                processors * 2,             // maximumPoolSize
                60L, TimeUnit.SECONDS,      // keepAliveTime
                new LinkedBlockingQueue<>(1000),  // 有界队列
                new ThreadFactory() {             // 命名线程
                    private final AtomicInteger idx = new AtomicInteger(1);
                    @Override
                    public Thread newThread(Runnable r) {
                        Thread t = new Thread(r);
                        t.setName("biz-pool-" + idx.getAndIncrement());
                        return t;
                    }
                },
                new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
        );
        return executor;
    }
}

关键语句说明:

  • LinkedBlockingQueue<>(1000):限制队列长度,避免无限堆积任务。
  • 自定义 ThreadFactory:给线程命名,排查问题时一眼能看出是哪个线程池。
  • AbortPolicy:拒绝时抛异常,提醒你线程池已经顶不住了,方便报警与扩容。

线程池中的拒绝策略

JDK 自带四种常用策略(RejectedExecutionHandler 实现):

  • AbortPolicy:直接抛 RejectedExecutionException(默认)。
  • CallerRunsPolicy:由提交任务的线程自己执行任务(降低新任务并发度)。
  • DiscardOldestPolicy:丢弃队列中最旧的任务,再尝试提交当前任务。
  • DiscardPolicy:静默丢弃当前任务。

实际项目中常用:AbortPolicy(+监控报警)或 CallerRunsPolicy(削峰)。也可以自定义策略做日志、降级等。

线程池的监控与常见坑

常见最佳实践与坑位:

  • 最佳实践:
    • 统一封装线程池创建,不在业务代码到处 new。
    • 监控:定期打印或上报线程池状态(poolSize、activeCount、queueSize、completedTaskCount)。
    • 为不同业务使用不同线程池(IO、计算、RPC 调用等),避免互相拖垮。
  • 典型坑:
    • 父任务在同一个线程池提交子任务并 get() 等待,而核心线程被父任务占满,子任务排队没线程可用,造成伪“死锁”。
    • 使用无界队列但 maximumPoolSize 设置很大,实际永远到不了扩容逻辑,队列无限堆积。
    • 关闭忘记调用 shutdown() / shutdownNow() 导致程序无法优雅退出。

简单监控示例(定时打印状态):

public static void printThreadPoolStatus(ThreadPoolExecutor threadPool) {
    ScheduledExecutorService monitor = new ScheduledThreadPoolExecutor(1);
    monitor.scheduleAtFixedRate(() -> {
        System.out.println("============");
        System.out.println("PoolSize: " + threadPool.getPoolSize());
        System.out.println("ActiveCount: " + threadPool.getActiveCount());
        System.out.println("CompletedTask: " + threadPool.getCompletedTaskCount());
        System.out.println("QueueSize: " + threadPool.getQueue().size());
        System.out.println("============");
    }, 0, 1, TimeUnit.SECONDS);
}