参数及工作原理

线程池是并发编程中非常重要的工具,它能有效管理线程资源,提升系统性能。我们以Java的ThreadPoolExecutor为例来说明,这是最经典、最通用的实现。

一、七大核心参数

ThreadPoolExecutor 的构造函数为例:

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

具体含义如下:

参数 含义
corePoolSize 核心线程数。即使空闲也会保留的线程数(除非设置allowCoreThreadTimeOut)。
maximumPoolSize 最大线程数。线程池允许创建的最大线程数量。
keepAliveTime 空闲线程存活时间。当当前线程数超过corePoolSize时,多余的空闲线程在等待新任务的最长时间,超时则销毁。
unit keepAliveTime的时间单位(秒、毫秒等)。
workQueue 任务阻塞队列。用于存放等待执行的任务(Runnable)。
threadFactory 线程工厂。用于创建新线程,可自定义线程名、优先级等。
handler 拒绝策略。当队列满且线程数已达maximumPoolSize时,对新提交的任务采取的处理策略。

二、工作原理(任务提交的执行流程)

当一个任务通过 execute()submit() 提交到线程池后,执行流程如下(核心逻辑):

提交新任务

核心线程池
是否未满?
线程数 < corePoolSize

创建新核心线程
执行任务

任务队列
是否已满?

任务进入队列
等待核心线程空闲

总线程数
< maximumPoolSize?

创建非核心线程
执行任务

执行拒绝策略

结束

更详细的文字步骤:

  1. 判断核心线程数

    • 当前运行的线程数 < corePoolSize:直接创建新线程(核心线程)来执行该任务。
    • 当前运行的线程数 >= corePoolSize:进入下一步。
  2. 判断任务队列是否已满

    • 任务队列 workQueue 未满:将任务放入队列中,等待核心线程空闲后拉取执行。
    • 任务队列 已满:进入下一步。
  3. 判断总线程数是否达最大线程数

    • 当前运行的线程数 < maximumPoolSize:创建新线程(非核心线程,可超时销毁)立即执行任务。
    • 当前运行的线程数 >= maximumPoolSize:线程池已饱和,执行拒绝策略
  4. 非核心线程的超时销毁

    • 当非核心线程空闲时间超过 keepAliveTime 后,会被终止回收(直到线程数回落到 corePoolSize)。

三、常见的工作队列(workQueue)特点

队列类型 特点 适用场景
SynchronousQueue 不存储任务,每个插入必须等待一个移除。容量为0,常用于将任务直接交给线程执行。 要求高吞吐量、快速响应的场景,配合maximumPoolSize较大时。
ArrayBlockingQueue 有界数组队列,容量固定。 资源控制严格,防止队列无限膨胀。
LinkedBlockingQueue 链表结构,默认无界(Integer.MAX_VALUE),也可设为有界。 任务积压容忍度高,但无界时需注意内存风险。
PriorityBlockingQueue 支持优先级排序的无界队列。 需要按优先级执行的任务。

四、常见拒绝策略(handler)

Java 提供了四种内置策略:

策略 行为 风险 / 场景
AbortPolicy(默认) 直接抛出 RejectedExecutionException 明确拒绝,需要上游捕获处理。
CallerRunsPolicy 由提交任务的线程(调用者)自己执行该任务 放缓任务提交速度,提供反压机制。
DiscardPolicy 静默丢弃该任务,不抛异常 允许丢失不重要任务。
DiscardOldestPolicy 丢弃队列中最老的任务,然后重新提交当前任务 追求最新任务的处理。

五、如何合理设置参数?

  • CPU 密集型任务corePoolSize = CPU 核心数 + 1(避免过多线程竞争CPU)。
  • I/O 密集型任务corePoolSize = CPU 核心数 × 2(或更高,因为大部分时间在等待I/O)。
  • 队列大小:根据任务到达率、任务处理时长、允许的最大等待时间计算。一般建议有界队列,防止内存溢出。
  • 拒绝策略:根据业务对丢失任务的容忍度选择,默认 AbortPolicy 较安全。

六、一个简单的例子

// 创建一个线程池:核心2线程,最大4线程,队列容量3,空闲线程存活1秒
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    2, 3, 1L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(3),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.AbortPolicy()
);

for (int i = 0; i < 8; i++) {
    final int taskId = i;
    pool.execute(() -> {
        System.out.println(Thread.currentThread().getName() + " 执行任务 " + taskId);
        try { Thread.sleep(100); } catch (InterruptedException e) {}
    });
}
pool.shutdown();

执行时会看到:前2个任务立即创建核心线程执行,第3-5个任务进入队列,第6-7个任务创建非核心线程执行,第8个任务触发拒绝策略(抛出异常)。

七、不推荐使用 Executors 工厂类的原因

  • Executors.newFixedThreadPoolnewSingleThreadExecutor 使用无界队列,高负载下可能导致 OOM。
  • Executors.newCachedThreadPool 最大线程数为 Integer.MAX_VALUE,可能创建过多线程导致系统资源耗尽。

所以生产环境建议直接使用 ThreadPoolExecutor 并明确指定参数。

如果你正在设计具体业务的线程池(比如异步处理、定时任务或结合虚拟线程),可以告诉我场景,我能给更针对性的参数建议。


怎么实现线程的重复使用

核心思路

线程池实现线程重复使用的核心原理是:让每个工作线程内部维持一个循环,不断从任务队列中取出新任务来执行,而不是执行完一个任务后就退出

下面以 Java 的 ThreadPoolExecutor 为例,详细拆解其实现机制。

一、关键角色:WorkerrunWorker

线程池内部把每个工作线程封装成一个 Worker 对象。Worker 本身是一个 Runnable,它持有一个 Thread 对象。

private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
    final Thread thread;   // 真正执行的线程
    Runnable firstTask;    // 创建时可能附带的一个初始任务
    // ...
}

当你提交任务时,线程池会根据规则创建 Worker(也就是创建新线程)。创建后,该 Workerthread 会启动,并运行 Workerrun 方法,最终进入 runWorker 方法。

二、核心循环:runWorker 方法(简化版)

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;   // 先取第一个任务
    w.firstTask = null;
    
    while (task != null || (task = getTask()) != null) {
        // 在这里执行任务
        task.run();     // 直接调用任务的 run 方法,不创建新线程
        task = null;    // 执行完后清空,然后循环 getTask()
    }
    // 跳出循环后,线程销毁(回收)
}

关键点:

  • while 循环会一直运行,直到 getTask() 返回 null
  • 每执行完一个任务,线程不会退出,而是继续调用 getTask()任务队列中获取下一个任务。
  • 获取到新任务后,再次调用 task.run() —— 同一个线程执行了不同任务,这就是“重复使用”。

三、任务获取:getTask() 如何工作?

getTask()workQueue(阻塞队列)中取出任务。它的伪逻辑是:

private Runnable getTask() {
    boolean timedOut = false;  // 上次 poll 是否超时

    for (;;) {
        // 判断当前线程数是否超过 corePoolSize 且允许超时回收
        boolean timed = allowCoreThreadTimeOut || 
                         wc > corePoolSize;

        if (timed && (wc > maximumPoolSize || (timedOut && wc > 1))) {
            // 如果允许超时且超时发生,减少线程计数并返回 null
            return null;
        }

        Runnable r;
        if (timed) {
            // 非核心线程:最多等 keepAliveTime,超时返回 null
            r = workQueue.poll(keepAliveTime, unit);
            if (r == null) timedOut = true;
        } else {
            // 核心线程:一直阻塞直到有任务
            r = workQueue.take();
        }
        if (r != null) return r;
        // 如果 r == null 且 timed == true,循环继续,下次判断会触发返回 null
    }
}
  • 核心线程:调用 workQueue.take(),队列为空时就一直阻塞等待,不会销毁。
  • 非核心线程(或开启了 allowCoreThreadTimeOut 的核心线程):调用 workQueue.poll(keepAliveTime, unit),等待一段时间后若仍没有新任务,就返回 null,导致 runWorkerwhile 循环结束,线程自然销毁。

四、整体复用流程总结

[线程池]  内部持有  Worker 线程(例如 3 个)

          ┌─────────────┐
          │  Worker 1   │ ─── while(true) {
          │             │       task = queue.take();  // 阻塞拿任务
          │  Thread A   │       task.run();           // 执行
          └─────────────┘     }

          ┌─────────────┐
          │  Worker 2   │ ─── 同样循环
          │  Thread B   │
          └─────────────┘

                ↑
                │ 提交多个任务(R1,R2,R3...)
                │
          ┌─────┴─────┐
          │ 工作队列  │
          │BlockingQueue│
          └───────────┘
  • 循环:每个工作线程都在不停循环:取任务 → 执行任务 → 再取任务
  • 阻塞:队列为空时,线程要么阻塞(核心线程),要么等待超时后退出(非核心线程)。
  • 不创建新线程:执行 task.run() 时,并没有 new Thread(task).start(),而是直接用当前线程执行,所以不会产生额外的线程开销。

五、一个直观的比喻

把线程池想象成一个餐厅的固定服务员团队

  • 每位服务员(线程)一直在餐厅里转悠。
  • 如果没有客人(任务),服务员就站在取菜口(阻塞队列)前等待。
  • 客人下单后,服务员从取菜口拿到单子(task),然后去执行(上菜、结账)。
  • 做完这单后,服务员马上回到取菜口等待下一个单子,而不是辞职走人
  • 只有在餐厅决定裁员(线程数超过 corePoolSize 且空闲太久)时,某些服务员才会离开(线程销毁)。

这就是“重复使用”的本质:线程的生命周期被延长,一直循环获取并执行新任务

更多推荐