跳转到正文
Siolin'Log
返回

Java 线程池:参数、任务调度与拒绝策略

线程池解决什么问题

如果每来一个任务都创建一个线程:

new Thread(task).start();

会有三个问题:

线程池的核心作用是:用有限的线程处理大量任务,并通过队列和拒绝策略控制系统压力。

它不仅是“复用线程”的工具,更重要的是一种资源治理机制。

线程池基本模型

ThreadPoolExecutor 的任务接收与工作线程模型

核心参数

Java 线程池的核心实现是:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize,
        maximumPoolSize,
        keepAliveTime,
        timeUnit,
        workQueue,
        threadFactory,
        rejectedExecutionHandler
);

corePoolSize:核心线程数

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

默认情况下,即使还没有任务,核心线程也不会立即创建,而是在任务到达时按需创建。

可以提前启动:

executor.prestartAllCoreThreads();

核心线程默认不会因空闲而销毁,除非开启:

executor.allowCoreThreadTimeOut(true);

maximumPoolSize:最大线程数

maximumPoolSize 是线程池允许存在的工作线程上限。超过 corePoolSize 的部分通常被称为非核心线程或临时线程:

最大线程数 = 核心线程 + 临时线程

但它是否真正有效,取决于任务队列。

例如使用无界队列:

new LinkedBlockingQueue<>()

任务几乎总能进入队列,线程池通常不会创建超过 corePoolSize 的线程,因此 maximumPoolSize 基本不起作用。代价是任务堆积不再受队列容量限制,持续过载时可能耗尽内存。

keepAliveTime:临时线程存活时间

当线程数量超过 corePoolSize 后,多出来的线程如果长时间没有任务,就会被销毁。

例如:

60, TimeUnit.SECONDS

表示非核心线程空闲 60 秒后退出。

workQueue:任务队列

用于暂存还没有被执行的任务。

常见队列如下:

threadFactory:线程工厂

负责创建线程。

生产环境至少应该为线程设置有意义的名字:

AtomicInteger threadNumber = new AtomicInteger();

ThreadFactory factory = runnable -> {
    Thread thread = new Thread(runnable);
    thread.setName("order-query-pool-" + threadNumber.incrementAndGet());
    thread.setUncaughtExceptionHandler((t, e) ->
            log.error("线程执行异常, thread={}", t.getName(), e)
    );
    return thread;
};

线程名非常重要。出现 CPU 飙高、死锁或任务卡住时,可以通过线程名快速定位所属业务。

实际项目中,可以使用 Guava 的 ThreadFactoryBuilder,或者自己维护线程编号。

RejectedExecutionHandler:拒绝策略

Java 线程池的拒绝策略 当线程池已经关闭,或者同时满足以下饱和条件时,线程池无法继续接收任务:

队列已满
+
线程数达到 maximumPoolSize

此时会执行拒绝策略。JDK 提供四种策略:

  1. AbortPolicy:抛出 RejectedExecutionException
  2. CallerRunsPolicy:在线程池未关闭时,由提交任务的线程自己执行任务,形成简单的反压;线程池已关闭时则丢弃任务。
  3. DiscardPolicy:直接丢弃任务,不抛异常。
  4. DiscardOldestPolicy:在线程池未关闭时丢弃队首任务,然后重新尝试提交当前任务;它仍可能再次被拒绝,而且通常不适合不能丢任务的业务。

任务提交后的完整流程

假设线程池参数如下:

corePoolSize = 4
maximumPoolSize = 8
queueCapacity = 100

任务提交过程是:

  1. 当前工作线程少于 4:创建核心线程执行任务
  2. 已经有 4 个线程:新任务进入队列
  3. 队列中已经有 100 个任务:继续创建临时线程
  4. 线程数量最多增加到 8
  5. 8 个线程都在忙,并且队列也满了:执行拒绝策略
  6. 压力下降后,多出来的 4 个临时线程会在空闲超时后销毁

对于这个有界队列配置,在某一时刻能够容纳的已接收任务数量不能简单理解为 8,而是:

正在执行的任务 + 队列中等待的任务
≈ maximumPoolSize + queueCapacity

这只是瞬时容量上限,不代表可持续吞吐量;能否在合理时间内完成还取决于任务耗时、到达速率和线程数配置。

execute 和 submit 区别

execute

executor.execute(() -> doSomething());

特点:

submit

Future<String> future = executor.submit(() -> "result");
String result = future.get();

特点:

submit() 有一个重要陷阱:异常会被封装进 Future

Future<?> future = executor.submit(() -> {
    throw new RuntimeException("执行失败");
});

如果完全不调用:

future.get();

异常可能不会出现在预期的日志中。

因此提交需要关注结果的任务时,应正确处理:

try {
    future.get();
} catch (ExecutionException e) {
    log.error("任务执行失败", e.getCause());
}


上一篇
Java synchronized:用法、Monitor 与锁竞争
下一篇
xv6 进程切换