Skip to content

线程池

为什么要用线程池?

线程池的核心作用是:复用线程、控制并发、统一管理任务

不用线程池时,每来一个任务就创建一个线程,会有几个问题:

频繁创建和销毁线程开销大

线程创建涉及系统资源分配,销毁也有成本。

线程数量不可控

请求量大时可能创建大量线程,导致 CPU 频繁上下文切换,甚至内存溢出。

提高响应速度

线程池中已有线程,任务来了可以直接执行,不需要临时创建线程。

方便统一管理

可以控制最大线程数、任务队列、拒绝策略、线程命名、异常处理等。

线程池通过复用线程降低创建销毁成本,通过队列和最大线程数控制并发数量,从而提高系统稳定性和响应速度。

ThreadPoolExecutor 七大参数?

ThreadPoolExecutor 构造方法的七个核心参数是:

java
new ThreadPoolExecutor(
    int corePoolSize,              // 1. 核心线程数
    int maximumPoolSize,           // 2. 最大线程数
    long keepAliveTime,            // 3. 空闲线程存活时间
    TimeUnit unit,                 // 4. 时间单位
    BlockingQueue<Runnable> workQueue,  // 5. 任务队列
    ThreadFactory threadFactory,   // 6. 线程工厂
    RejectedExecutionHandler handler  // 7. 拒绝策略
);

1. corePoolSize:核心线程数

线程池长期保留的线程数量。

即使线程空闲,只要没有设置 allowCoreThreadTimeOut(true),核心线程一般不会被回收。

2. maximumPoolSize:最大线程数

线程池允许创建的最大线程数量。

当任务队列满了,并且当前线程数小于最大线程数时,会继续创建非核心线程。

3. keepAliveTime:空闲线程存活时间

非核心线程空闲超过这个时间后,会被回收。

如果设置了 allowCoreThreadTimeOut(true),核心线程也可以被回收。

4. unit:时间单位

keepAliveTime 的时间单位,比如:

  • TimeUnit.SECONDS
  • TimeUnit.MILLISECONDS

5. workQueue:任务队列

当核心线程都在忙时,新任务会进入任务队列等待执行。

常见队列:

  • ArrayBlockingQueue
  • LinkedBlockingQueue
  • SynchronousQueue
  • PriorityBlockingQueue
  • DelayQueue

6. threadFactory:线程工厂

用于创建线程,可以自定义线程名称、是否守护线程、优先级等。

一般建议自定义线程名,方便排查问题。

java
ThreadFactory factory = r -> new Thread(r, "order-pool-" + index.getAndIncrement());

7. handler:拒绝策略

当线程数达到最大线程数,并且队列也满了,再提交任务时触发拒绝策略。

线程池执行任务的流程?

线程池执行任务的核心流程是:

第一步:判断核心线程数是否已满

如果当前线程数 < corePoolSize,直接创建核心线程执行任务。

第二步:核心线程满了,任务进入队列

如果当前线程数 >= corePoolSize,任务会尝试放入 workQueue。

第三步:队列满了,尝试创建非核心线程

如果任务队列也满了,并且当前线程数 < maximumPoolSize,则创建非核心线程执行任务。

第四步:线程数也满了,执行拒绝策略

如果当前线程数已经达到 maximumPoolSize,并且队列也满了,就触发拒绝策略。

先用核心线程执行,核心线程满了就进队列,队列满了就创建非核心线程,线程数达到最大值后执行拒绝策略。

核心线程数怎么设置?

核心线程数没有固定答案,要看任务类型。

CPU 密集型任务

比如计算、加密、压缩、图片处理等,主要消耗 CPU。

推荐:

java
核心线程数 = CPU 核数 + 1

或者:

java
核心线程数 = CPU 核数

原因是线程太多会导致频繁上下文切换,反而降低性能。

IO 密集型任务

比如数据库操作、RPC 调用、文件读写、网络请求等,大部分时间在等待 IO。

推荐:

java
核心线程数 = CPU 核数 * 2

或者更通用的估算公式:

java
线程数 = CPU 核数 * (1 + IO等待时间 / CPU计算时间)

比如一个任务 80% 时间在等待 IO,20% 时间在计算:

java
线程数 = CPU 核数 * (1 + 80 / 20)
线程数 = CPU 核数 * 5

实际生产建议

不要只靠公式,应该结合:

  • CPU 使用率
  • RT 响应时间
  • QPS
  • 队列堆积情况
  • 内存占用
  • 下游服务承载能力

CPU 密集型一般设置为 CPU 核数或 CPU 核数 + 1;IO 密集型可以适当调大,一般是 CPU 核数的 2 倍或根据等待时间比例估算,最终要通过压测确定。

拒绝策略有哪些?

JDK 默认提供四种拒绝策略。

1. AbortPolicy(默认)

直接抛出异常 RejectedExecutionException

适合希望明确感知任务被拒绝的场景。

2. CallerRunsPolicy

由提交任务的线程自己执行任务。

比如主线程提交任务,如果线程池满了,就让主线程自己执行。

优点是可以降低任务提交速度,起到一定限流作用。

3. DiscardPolicy

直接丢弃新任务,不抛异常。

风险较高,因为任务会悄悄丢失。

4. DiscardOldestPolicy

丢弃队列中最老的任务,然后尝试提交当前任务。

也有任务丢失风险。

生产中常见做法

一般会自定义拒绝策略,比如:

  • 打日志
  • 记录监控指标
  • 任务持久化到 MQ 或数据库
  • 告警通知
  • 根据业务降级

execute() 和 submit() 区别?

execute()

用于提交没有返回值的任务。

java
threadPool.execute(() -> {
    System.out.println("执行任务");
});

特点:

  • 没有返回值
  • 只能提交 Runnable
  • 任务异常会直接抛出,通常会被线程的 UncaughtExceptionHandler 处理

submit()

用于提交有返回值或需要拿到执行结果的任务。

java
Future<Integer> future = threadPool.submit(() -> {
    return 1 + 1;
});

Integer result = future.get();

特点:

  • 有返回值,返回 Future
  • 可以提交 Runnable,也可以提交 Callable
  • 异常不会直接抛出,而是封装在 Future 中
  • 调用 future.get() 时才会抛出 ExecutionException

核心区别:execute() 只负责提交任务,没有返回值;submit() 会返回 Future,可以获取结果或异常。submit() 的异常会被封装到 Future 中,只有调用 get() 时才能感知。

线程池为什么不建议用 Executors 创建?

因为 Executors 提供的快捷方法容易隐藏风险,尤其是队列无界或线程数过大,可能导致 OOM。

1. Executors.newFixedThreadPool()

底层使用 LinkedBlockingQueue,默认容量是 Integer.MAX_VALUE

如果任务提交速度大于消费速度,队列会无限堆积,可能导致内存溢出。

2. Executors.newSingleThreadExecutor()

也是使用无界队列。

如果任务过多,也可能 OOM。

3. Executors.newCachedThreadPool()

底层最大线程数是 Integer.MAX_VALUE

在高并发场景下可能创建大量线程,导致系统资源耗尽。

4. Executors.newScheduledThreadPool()

最大线程数也是 Integer.MAX_VALUE

也存在资源耗尽风险。

推荐做法

手动使用 ThreadPoolExecutor 创建线程池,明确指定:

  • corePoolSize
  • maximumPoolSize
  • workQueue
  • threadFactory
  • handler

Executors 创建线程池虽然方便,但很多默认配置使用无界队列或过大的最大线程数,容易导致 OOM,所以生产环境建议手动创建 ThreadPoolExecutor,明确参数。

如何优雅关闭线程池?

线程池关闭常用两个方法:

shutdown()

java
threadPool.shutdown();

表示不再接收新任务,但会继续执行已经提交的任务,包括队列中的任务。

shutdownNow()

java
threadPool.shutdownNow();

尝试立即关闭线程池:

  • 不再接收新任务
  • 尝试中断正在执行的任务
  • 返回队列中尚未执行的任务

注意:shutdownNow() 不一定能立即停止任务,因为线程中断需要任务自己响应。

优雅关闭推荐写法

java
public void shutdownThreadPool(ExecutorService executor) {
    executor.shutdown();
    try {
        if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
            executor.shutdownNow();

            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                System.err.println("线程池未能正常关闭");
            }
        }
    } catch (InterruptedException e) {
        executor.shutdownNow();
        Thread.currentThread().interrupt();
    }
}

这段代码的逻辑是:

  1. 先调用 shutdown(),停止接收新任务
  2. 等待一段时间,让已提交任务执行完
  3. 如果超时还没结束,再调用 shutdownNow()
  4. 如果当前线程被中断,要恢复中断标记

优雅关闭线程池一般先调用 shutdown(),然后用 awaitTermination() 等待任务执行完成,如果超时再调用 shutdownNow(),并且在捕获 InterruptedException 后恢复中断状态。