站在JDK的角度理解线程池:从execute 到 getTask 一篇讲透

目录


一、为什么要用线程池?

线程本身是用来异步处理任务 的,而且可以并发处理多个任务。但如果无限制地 new Thread(),问题就来了:

  • 创建/销毁成本高:每次创建线程都要向 OS 申请栈空间、注册线程;
  • 切换成本高:线程数远超 CPU 核数时,上下文切换吃掉大量 CPU;
  • 不可控 :无法统一限流、监控、管理,极端情况直接 OOM: unable to create new native thread。

于是"池化技术"登场------提前养一批线程,反复复用,按需分配。JDK 文档给出了线程池的四大好处:

好处 说明
降低资源消耗 重复利用已创建的线程,降低创建和销毁造成的损耗
提高响应速度 任务到达时,不需要等待线程创建,直接 execute 即可
提高可管理性 统一分配、调优、监控(getPoolSize()、getActiveCount()、getTaskCount())
提供更多强大功能 可扩展性:beforeExecute / afterExecute 钩子、UncaughtExceptionHandler、定时调度

一句话抓住核心思想:用固定的(上限可控的)线程数量,去消费不定的 task ------ 这就是所有源码要基于它理解的根本。

整体是一个生产者-消费者模型:

flowchart LR subgraph P[生产者] A[生产者A] B[生产者B] C[生产者C] end subgraph TP[线程池] EX[execute task] Q[(阻塞队列<br/>task task task task)] W0[线程0] W1[线程1] W2[线程2] W3[线程3] end A --> EX B --> EX C --> EX EX -- offer --> Q Q -- take --> W0 Q -- take --> W1 Q -- take --> W2 Q -- take --> W3

二、先认清 JDK 给我们的"开箱即用"线程池

Executors 工具类封装了几种常见线程池:

工厂方法 底层实现 特点 风险点(补充)
newFixedThreadPool(n) ThreadPoolExecutor,core = max = n,LinkedBlockingQueue(无界) 线程数固定,超出任务排队 无界队列 → 任务堆积导致 OOM
newSingleThreadExecutor() 同上,core = max = 1 单线程串行,适合保证顺序执行 同上
newCachedThreadPool() core = 0,max = Integer.MAX_VALUE,SynchronousQueue,keepAlive = 60s 有任务即创建,空闲 60s 回收 线程数无上限 → 创建线程 OOM
newScheduledThreadPool(n) ScheduledThreadPoolExecutor 延迟 / 周期执行,类似定时器 队列无界
newWorkStealingPool() ForkJoinPool (不是 ThreadPoolExecutor 的子类) 工作窃取,适合耗时的并行计算型任务,能充分利用 CPU 对阻塞型任务不友好

关于 newWorkStealingPool:它是 ForkJoinPool 的扩展,适合做可拆分、耗时的并行操作(分治 + 工作窃取,空闲线程去别的队列"偷"任务,减少空闲)。

除了 newWorkStealingPool,其余全是 ThreadPoolExecutor 的一层封装,生产环境直接 new ThreadPoolExecutor(...)。

理由是:

  1. 逼着你去把 corePoolSize / maximumPoolSize / keepAliveTime / workQueue / handler 这几个参数搞清楚;
  2. 能做的事更多------可以挂监控(自定义 afterExecute 统计耗时)、自定义拒绝策略、自定义线程名/异常处理器;
  3. 规避 Executors 那些"无界"实现的 OOM 风险,这也是阿里《Java 开发手册》里明确要求的一条:

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

先跑个最简单的例子感受一下(注意:把 new Thread(runnable) 作为参数传给 execute 其实是不推荐的写法,原因见第七节):

java 复制代码
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 10; i++) {
    executorService.execute(new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "执行任务");
    }));
}
executorService.shutdown();

输出只会是 pool-1-thread-1 / pool-1-thread-2 两两交替------10 个任务,只有 2 个线程在干活,这就是"固定线程数消费不定量任务"。

再来看一个带耗时的版本,50 个任务、5 个线程、每个睡 1 秒:

java 复制代码
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 50; i++) {
    executor.execute(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("测试" + Thread.currentThread().getName());
    });
}
executor.shutdown();

大约每 1 秒打印 5 条,10 秒左右跑完------5 个线程轮番复用,而不是创建 50 个线程。


三、初始化:七个参数与构造器校验

官方给出的参数标准:

java 复制代码
public ThreadPoolExecutor(int corePoolSize,              // 核心线程数
                          int maximumPoolSize,           // 最大线程数
                          long keepAliveTime,            // 存活时间:除核心线程外,空闲线程多久后被回收
                          TimeUnit unit,                 // 存活时间单位
                          BlockingQueue<Runnable> workQueue,   // 阻塞队列,待执行的 task 都在这里
                          ThreadFactory threadFactory,   // 线程工厂
                          RejectedExecutionHandler handler) { // 拒绝策略:队列满 + 线程数达 maximum 时触发
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);   // 统一转成纳秒
    this.threadFactory = threadFactory;
    this.handler = handler;
}

任务队列怎么用 什么时候用

队列 特点 适用
ArrayBlockingQueue 有界,数组实现,一把锁 生产首选,可控,能触发扩容到 max 与拒绝
LinkedBlockingQueue 链表实现,读写两把锁,无参构造时容量 = Integer.MAX_VALUE 想用它也可以,但必须指定容量
SynchronousQueue 不存储元素,put 必须有线程 take CachedThreadPool 那种"来了就建线程"
PriorityBlockingQueue 无界,按优先级出队 需要优先级调度(注意 still 无界)
DelayQueue 延迟队列,ScheduledThreadPoolExecutor 用它 定时任务
LinkedTransferQueue 无锁实现,transfer 语义 高吞吐场景

划重点:线程池里"最大线程数"能不能用得着,取决于队列是否有界 。用无界 LinkedBlockingQueue 时 maximumPoolSize 形同虚设------因为队列永远 offer 成功,永远走不到"尝试创建非核心线程"那一步。


四、源码主线:从一个任务的一生说起

4.1 全景流程图

flowchart TD S[主线程 execute command] --> A{核心线程数是否已满?} A -- 否 --> AW1[addWorker command, true<br/>创建核心线程直接执行] A -- 是 --> B{workQueue.offer 成功?} B -- 是 --> RC[二次检查 recheck] RC --> RC1{线程池已关闭?} RC1 -- 是 --> RM[remove 撤销入队] RM --> REJ[reject 拒绝] RC1 -- 否 --> RC2{workerCount == 0?} RC2 -- 是 --> AW2[addWorker null, false<br/>补一个消费者线程] RC2 -- 否 --> WAIT[任务在队列中等待] B -- 否 --> AW3[addWorker command, false<br/>尝试创建非核心线程] AW3 -- 成功 --> RUN[新线程执行任务] AW3 -- 失败 --> REJ
scss 复制代码
主线程 execute(task)
        │
        ▼
 ① 核心线程数没满? ── 是 ──► addWorker(task, true)  创建一个核心 Worker 直接执行
        │ 否
        ▼
 ② 任务成功入队 workQueue.offer()? ── 是 ──► 二次检查 recheck
        │ 否                                   ├─ 池已关闭 → 出队 + 拒绝
        ▼                                      └─ 线程数为 0 → addWorker(null, false) 兜底
 ③ addWorker(task, false) 创建非核心线程(没超过 maximumPoolSize)
        │ 失败
        ▼
 ④ reject(task) 走拒绝策略

大概就是上述的流程吧

4.2 ctl:一个 int 同时存状态和线程数

先看 JDK 里最"巧"的设计:

java 复制代码
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;      // 29
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1; // 2^29 - 1

private static final int RUNNING    = -1 << COUNT_BITS;   // 高 3 位 111
private static final int SHUTDOWN   =  0 << COUNT_BITS;   // 000
private static final int STOP       =  1 << COUNT_BITS;   // 001
private static final int TIDYING    =  2 << COUNT_BITS;   // 010
private static final int TERMINATED =  3 << COUNT_BITS;   // 011

private static int runStateOf(int c)     { return c & ~COUNT_MASK; } // 取高 3 位
private static int workerCountOf(int c)  { return c & COUNT_MASK;  } // 取低 29 位
private static int ctlOf(int rs, int wc) { return rs | wc; }         // 合并

ctl 的二进制布局(一共 32 位):

text 复制代码
 31        29 28                                              0
┌────────────┬──────────────────────────────────────────────────┐
│  高 3 位    │                    低 29 位                       │
│ runState   │                  workerCount                     │
│ 线程池状态  │                   线程数量                        │
└────────────┴──────────────────────────────────────────────────┘
   111 RUNNING   000 SHUTDOWN   001 STOP   010 TIDYING   011 TERMINATED
  • 高 3 位 = 线程池状态(可表示 8 种,JDK 用了 5 种)
  • 低 29 位 = 线程数量 (最大 2^29 - 1 ≈ 5.36 亿)

五个状态的含义与流转:

状态 值 说明
RUNNING 111 接受新任务,并处理队列中的任务
SHUTDOWN 000 不接受新任务,但会把队列中的任务处理完
STOP 001 不接受新任务,不处理队列任务,并中断正在执行的任务
TIDYING 010 所有任务已终止、workerCount 为 0,即将执行 terminated()
TERMINATED 011 terminated() 执行完毕
scss 复制代码
RUNNING ──shutdown()──► SHUTDOWN ──队列空且线程数0──► TIDYING ──► TERMINATED
   │                        │                            ▲
   └────shutdownNow()──────►└───STOP───线程数0────────────┘

为什么要这么打包? 一次 CAS 就能同时维护"状态 + 线程数",减少锁和 CAS 次数。ThreadPoolExecutor 里只有 workers 集合的操作才用 mainLock(ReentrantLock),ctl 的读写都是无锁的 CAS 自旋------这是高性能并发设计的经典手法。

4.3 execute():三段式提交逻辑

java 复制代码
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    // ① 线程数 < corePoolSize,直接创建核心线程执行
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // ② 线程池处于 RUNNING 且任务入队成功
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 入队后二次检查:期间线程池被关闭 → 撤销入队 + 拒绝
        if (!isRunning(recheck) && remove(command))
            reject(command);
        // 二次检查:线程数为 0(比如核心线程刚好全部超时退出)→ 建一个非核心线程兜底
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // ③ 入队失败(队列满)→ 尝试建非核心线程,还是失败 → 拒绝
    else if (!addWorker(command, false))
        reject(command);
}

JDK 源码注释里也把它总结为 3 steps。在这里补充一下:

  1. 为什么入队后要 recheck?
    • 入队与 shutdown() 之间存在竞态:可能刚入队,池子就被关了 → 需要 remove 把任务撤回并拒绝;
    • 也可能在检查线程数之后,最后一个工作线程恰好因 getTask() 返回 null 而退出 → 线程数为 0,队列里却还有任务,必须补建一个线程 (addWorker(null, false),注意 firstTask 是 null,只负责消费队列)。
  2. 为什么 addWorker(null, false) 里 core = false? 这里只是补个"消费者",不关心是核心还是非核心,传 false 会按 maximumPoolSize 校验------已经 shutdown 的情况下(shutdown 后队列非空),也会走这条逻辑把剩余任务消费完。
  3. 顺序为什么是"先核心、再队列、再非核心"? 这是线程池最关键的设计取舍:优先复用 + 缓冲,而不是无脑扩线程。任务堆积优先排队(避免频繁建线程),只有在队列也装满时才扩容到 max,最后才拒绝。容量有限才能形成"背压",让上游感知到。

4.4 addWorker():CAS 抢占 + 加锁入集合 + 启动线程

java 复制代码
private boolean addWorker(Runnable firstTask, boolean core) {
    retry:                                  // goto 标签,配合 break/continue 跳出多层循环
    for (int c = ctl.get();;) {
        // 检查状态:>= SHUTDOWN 且(>= STOP 或 firstTask 非空 或 队列已空)→ 不能再创建
        if (runStateAtLeast(c, SHUTDOWN)
            && (runStateAtLeast(c, STOP) || firstTask != null || workQueue.isEmpty()))
            return false;

        for (;;) {
            // 数量检查:核心线程比 corePoolSize,非核心比 maximumPoolSize
            if (workerCountOf(c) >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                return false;
            // CAS 把 workerCount +1
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();                  // CAS 失败,重读 ctl
            if (runStateAtLeast(c, SHUTDOWN))
                continue retry;             // 状态变了,回到外层重新做状态检查
            // 否则只是因为并发改线程数导致 CAS 失败,重试内层循环
        }
    }

    boolean workerStarted = false, workerAdded = false;
    Worker w = null;
    try {
        w = new Worker(firstTask);          // Worker 实现了 Runnable,内部持有 Thread
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                int c = ctl.get();
                // 持锁后再次检查状态,防止拿到锁之前池子被关
                if (isRunning(c) || (runStateLessThan(c, STOP) && firstTask == null)) {
                    if (t.getState() != Thread.State.NEW)
                        throw new IllegalThreadStateException();
                    workers.add(w);
                    workerAdded = true;
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;   // 记录历史峰值,供监控用
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                t.start();                      // 启动,会回调 Worker.run() → runWorker()
                workerStarted = true;
            }
        }
    } finally {
        if (!workerStarted)
            addWorkerFailed(w);                 // 兜底回滚:移除 worker + workerCount-1 + tryTerminate
    }
    return workerStarted;
}

讲一下 retry: 标签:Java 的 break retry / continue retry 相当于 goto 到外层循环。在下面给一个 goto demo案例:

java 复制代码
retry:
for (int i = 0; i < 3; i++) {
    for (int j = 3; j < 10; j++) {
        if (j == 4) {
            break retry;      // 跳出外层整个循环
        }
        if (j == 7) {
            continue retry;   // 跳过 outer 循环当前轮次,i++ 后继续
        }
        System.out.println(i + ":" + j);
    }
}

上面 demo 的 if (j == 4) break retry; 一旦命中,后面的 j == 7 分支就不再会被执行到------两者的执行效果都是"结束内层、进入下一轮外层"。理解 retry 标签的关键是:标签加在最外层 for 上,break retry = 结束整个双层循环,continue retry = 结束本次外层循环迭代。

这里的设计要点:

  • 双层循环各有分工:外层循环只负责"状态检查",内层循环负责"数量检查 + CAS 抢名额"。CAS 失败只重试内层,成本更低;如果状态变了才回到外层。
  • CAS 加名额先行,加锁入集合在后 :先在 ctl 上原子地"预定"一个线程名额,再去 mainLock 里 workers.add(),这样并发创建不会超过上限。
  • mainLock 保护的是 workers 这个 HashSet 以及 largestPoolSize / completedTaskCount 等统计量,不是保护数量上限(那是 CAS 干的)。

4.5 Worker:一个自带锁的"打工人"

java 复制代码
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
    final Thread thread;
    Runnable firstTask;
    volatile long completedTasks;

    Worker(Runnable firstTask) {
        setState(-1);  // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }

    public void run() { runWorker(this); }
    // ... tryLock/lock/unlock 都是对 AQS state 的简单包装(0 未锁,1 已锁)
}

setState(-1)是干嘛的呢?

  • Worker 继承 AbstractQueuedSynchronizer ,用它实现了一把不可重入的互斥锁 ,比 ReentrantLock 更轻量,而且"不可重入"能防止任务里再去调用池子的控制方法时重复加锁。
  • state = -1 是"未启动"的中间态:此时 tryLock() 永远失败(state != 0),于是 interruptIdleWorkers 不会中断一个还"没真正跑起来"的线程。runWorker 里第一件事就是 w.unlock()(把 state 置 0),从此才允许被中断。
  • 为什么要这把锁?区分"空闲线程"和"正在执行的线程" :只有 tryLock() 成功(说明没在跑任务)的 Worker,才允许 interrupt()。这样 shutdown() 派发的中断总是能唤醒阻塞在 take() 上的空闲线程,而不会打断正在执行的任务。

4.6 runWorker():线程复用的心脏

java 复制代码
final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;     // 首次执行时可能有 firstTask
    w.firstTask = null;
    w.unlock();                      // allow interrupts:state 从 -1 置 0
    boolean completedAbruptly = true;
    try {
        // ★ 只要还能取到任务就一直循环:这就是"线程复用"的实现
        while (task != null || (task = getTask()) != null) {
            w.lock();                // 执行期间加锁 → 标记"忙",防止被空闲中断
            // 若池子处于 STOP 及以上,确保线程被中断;否则清理掉残留中断标记
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);     // 钩子:任务前
                Throwable thrown = null;
                try {
                    task.run();              // 真正执行任务
                } catch (RuntimeException x) { thrown = x; throw x;
                } catch (Error x) { thrown = x; throw x;
                } catch (Throwable x) { thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown);   // 钩子:任务后(无论是否异常)
                }
            } finally {
                task = null;             // 置空,防止内存泄漏
                w.completedTasks++;      // 累计该 worker 完成数
                w.unlock();
            }
        }
        completedAbruptly = false;       // 正常退出(getTask 返回 null)
    } finally {
        processWorkerExit(w, completedAbruptly);   // 收尾 + 可能补线程
    }
}

关键点:

  • 线程不会在任务结束后销毁 ,而是 while 循环回到 getTask() 继续取任务;getTask() 返回 null 才是线程结束的唯一正常出口。
  • completedAbruptly 记录"是否因为用户任务抛异常而突然退出":如果是,processWorkerExit 会补建一个线程 保证线程数不缩水;如果是正常退出,则按需补充到 corePoolSize(或 allowCoreThreadTimeOut 时补到 0)。
  • catch (Throwable x) { throw new Error(x); } 的意义:Runnable.run() 的签名不允许抛受检异常,JDK 把任意 Throwable 包装成 Error 抛出,同时把原始异常交给 afterExecute------所以你看到的"任务异常没有打印",往往是因为你提交的是 new Thread(任务) 而不是直接提交任务(见第六节避坑)。
  • beforeExecute / afterExecute / terminated 是三个可覆写钩子,监控告警就靠它们。

4.7 getTask():阻塞、超时与回收

java 复制代码
private Runnable getTask() {
    boolean timedOut = false;   // 上一次 poll 是否超时
    for (;;) {
        int c = ctl.get();
        // 池子已 SHUTDOWN 且(已 STOP 或队列空了)→ 线程数减一,返回 null,线程结束
        if (runStateAtLeast(c, SHUTDOWN)
            && (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);
        // 是否允许超时淘汰:allowCoreThreadTimeOut 为 true,或线程数已超过 corePoolSize
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        // 超时了 / 超最大线程数,且(线程数 > 1 或 队列空)→ 数量减一,返回 null → 该线程结束
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;   // CAS 失败重试
        }

        try {
            // timed = true:最多等 keepAliveTime,超时返回 null(下面置 timedOut=true)
            // timed = false:无超时地阻塞等待
            Runnable r = timed
                ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)
                : workQueue.take();
            if (r != null)
                return r;
            timedOut = true;      // ★ 这次超时了,下轮循环走到上面就返回 null 退出
        } catch (InterruptedException retry) {
            timedOut = false;     // 被中断不算超时,重新判断状态
        }
    }
}

这段是整个线程池"能收能放"的核心:

  • take() 是永久阻塞 :核心线程即使闲着也挂在这儿等,不会被回收(除非 allowCoreThreadTimeOut(true))。
  • poll(keepAliveTime) 是限时阻塞 :超过 keepAliveTime 还取不到任务就返回 null → timedOut = true → 下一轮循环命中回收条件 → workerCount-1 → 线程结束、被回收。
  • wc > 1 || workQueue.isEmpty() 这个条件很精妙:永远至少保留一个线程 ,避免"队列里刚来任务,最后那个线程却恰好超时退出"导致任务没人消费(即使退出,tryTerminate 也会在必要时补一个空闲线程,这里主要是为了避免高频抖动)。

4.8 拒绝策略

线程池内置 4 种 RejectedExecutionHandler:

策略 行为 风险/适用
AbortPolicy(默认) 抛 RejectedExecutionException 让调用方感知并处理,适合核心业务
CallerRunsPolicy 由提交任务的线程自己执行 优雅的背压:提交线程被占用,天然降低提交速率,不丢任务
DiscardPolicy 静默丢弃 可能假死,务必打日志(需要自己包一层)
DiscardOldestPolicy 丢弃队列头(最老的任务)再重试提交 只适合"新的更重要"的场景

自定义一个拒绝策略:

java 复制代码
public class ExecJavaTemplate implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println("把任务继续执行完" + r.toString());
        executor.execute(r);   // ⚠️ 有栈溢出风险,见下方说明
    }
}

配合:

java 复制代码
ThreadPoolExecutor executorService = new ThreadPoolExecutor(2, 3, 10, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<>(3), new ExecJavaTemplate());
for (int i = 0; i < 10; i++) {
    executorService.execute(new Thread(() -> {
        try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println(Thread.currentThread().getName() + "执行任务");
    }));
}

⚠️ 提醒一下(这段写法有坑,别照抄) :拒绝策略里直接写 executor.execute(r) 是危险的 。如果此时池子仍是满的,execute 会再次触发拒绝 → 再调用 rejectedExecution → 无限递归 → StackOverflowError;如果并发任务耗时较长,还可能造成长期自旋式的"忙等待"。而且队列再次满时这个任务依然会被拒绝。

正确的 "重试"必须满足至少一个条件:限次重试 + 退避(Thread.sleep / LockSupport.parkNanos) ,或者交给独立的补偿线程池 / MQ 落库。例如:

java 复制代码
/**
 * 限次重试 + 指数退避的拒绝策略。
 * 注意:不能直接 executor.execute(r),否则池子仍满时会递归回本方法 → StackOverflowError
 */
public class RetryRejectionHandler implements RejectedExecutionHandler {
    private static final Logger log = LoggerFactory.getLogger(RetryRejectionHandler.class);
    private final int maxRetry;

    public RetryRejectionHandler(int maxRetry) { this.maxRetry = maxRetry; }

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        for (int i = 0; i < maxRetry; i++) {
            if (e.isShutdown()) { break; }
            try {
                if (e.getQueue().offer(r, 50L << i, TimeUnit.MILLISECONDS)) {  // 50ms, 100ms, 200ms ...
                    return;
                }
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        // 兜底:落盘 / 丢到 MQ / 告警,而不是死等
        log.warn("[rejected] task dropped after {} retries: {}", maxRetry, r);
    }
}

生产上更推荐的做法,按优先级排序:

  1. CallerRunsPolicy 做背压:最简单的"不丢任务"方案,代价是提交线程被阻塞,可能会拖慢上游;
  2. 溢出任务落库 / 投递 MQ 异步补偿,保证数据不丢;
  3. 无论选哪种,都必须打点告警(拒绝次数、队列长度),否则就是静默故障。

五、线程的回收与线程池关闭

5.1 线程回收

触发路径其实只有一条:getTask() 返回 null → runWorker 退出循环 → processWorkerExit 收尾。

具体条件(在 getTask 中判断):

  1. 池子已 SHUTDOWN 且队列已空 → 回收;
  2. 池子已 STOP(及以上,shutdownNow 之后)→ 回收;
  3. timed && timedOut(keepAliveTime 到期且队列空)→ 回收;
  4. wc > maximumPoolSize(比如 setMaximumPoolSize 调小了)→ 回收。

所以"核心线程永不回收"其实是可以打破的:executor.allowCoreThreadTimeOut(true),此时 timed 恒为 true,核心线程闲置超过 keepAliveTime 也会被回收(注意:keepAliveTime 必须 > 0,否则抛 IllegalArgumentException)。

5.2 shutdown()

java 复制代码
public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(SHUTDOWN);   // 状态迁移到 SHUTDOWN:不再接受新任务
        interruptIdleWorkers();      // 只中断"空闲"的 worker(被锁住 = 忙,不动)
        onShutdown();                // 钩子,ScheduledThreadPoolExecutor 在这里取消延迟任务
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}

课件强调的一点非常关键:shutdown() 不会终止正在运行的线程。

它只做三件事:

  1. 把状态改成 SHUTDOWN,之后 execute 提交的新任务会被拒绝;
  2. interruptIdleWorkers() 只中断空闲 的 worker------靠的是 w.tryLock() 成功(见 4.5),正在执行任务的线程不会被中断,它会把手上的任务执行完;
  3. tryTerminate() 尝试推进到终止状态。

之后那些空闲线程在 getTask() 里被唤醒,发现 SHUTDOWN && 队列空,返回 null 而自然结束。队列中已有的任务会被执行完 ------所以 shutdown() 是"优雅关闭"。

5.3 shutdownNow()

java 复制代码
public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(STOP);     // 直接到 STOP
        interruptWorkers();        // 中断所有 worker(包括正在执行的)
        tasks = drainQueue();      // 把队列里没执行的任务全部取出返回
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}

shutdown和shutdownNow之间的对比:

shutdown() shutdownNow()
状态 SHUTDOWN STOP
接受新任务 否 否
队列中任务 继续执行完 不再执行 (drainQueue 返回给你)
正在执行的任务 不中断 尝试中断 (靠 Thread.interrupt,不响应中断的任务依然跑着)
返回 void List<Runnable> 未执行任务

补充 :shutdown()/shutdownNow() 都是异步 的,返回时线程池并未真正终止。要等待终止请用 awaitTermination(timeout, unit)

5.4 状态流转收口:tryTerminate()

java 复制代码
final void tryTerminate() {
    for (;;) {
        int c = ctl.get();
        // RUNNING、已 TIDYING 及以上、或 SHUTDOWN 但队列非空 → 不能终止
        if (isRunning(c) || runStateAtLeast(c, TIDYING)
            || (runStateLessThan(c, STOP) && !workQueue.isEmpty()))
            return;
        if (workerCountOf(c) != 0) {        // 还有线程 → 只中断一个空闲 worker 传播信号
            interruptIdleWorkers(ONLY_ONE);
            return;
        }
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {   // TIDYING
                try {
                    terminated();                           // 钩子
                } finally {
                    ctl.set(ctlOf(TERMINATED, 0));           // TERMINATED
                    termination.signalAll();                 // 唤醒所有 awaitTermination 的线程
                }
                return;
            }
        } finally {
            mainLock.unlock();
        }
    }
}

interruptIdleWorkers(ONLY_ONE) 的注释解释得很清楚:只中断一个空闲线程 ,靠这个被唤醒的线程继续检查状态并退出,进而再次触发 tryTerminate,形成"接力式"的终止传播。shutdown() 之所以中断全部空闲线程,是为了让多余的线程尽快退出,不用等某个"拖后腿"的任务。

顺带一提 shutdown() 里的 interruptIdleWorkers(),真实实现就是一个 tryLock 判断:

java 复制代码
private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers) {
            Thread t = w.thread;
            if (!t.isInterrupted() && w.tryLock()) {   // tryLock 成功 = 此刻空闲
                try {
                    t.interrupt();                     // 唤醒阻塞在 take() 上的线程
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            if (onlyOne) break;
        }
    } finally {
        mainLock.unlock();
    }
}

这十几行代码就是"shutdown 不会杀正在跑的任务"的答案 :runWorker 在执行任务期间持有 w.lock(),tryLock() 必然失败,所以忙碌的 worker 会被跳过。


六、参数怎么设、怎么"动态"设

6.1 线程数到底设多少?

按照经典的阻塞系数版本:

scss 复制代码
线程数 = CPU 可用核心数 / (1 - 阻塞系数)
阻塞系数 = 阻塞时间 / (阻塞时间 + 计算时间)      // 取值 (0, 1)

结论:

  • CPU 密集型 (W/C ≈ 0):线程数 ≈ N_cpu + 1(或直接 N_cpu),线程多了只会增加上下文切换;
  • IO 密集型 (W/C 大):线程数可以远大于核数,N_cpu × (1 + W/C);
  • 线程数最少也要 >= CPU 核心数(除非全部是长阻塞等待型);
  • 公式只是起点,压测才是终点 :线上还要看 GC、上下文切换(vmstat/pidstat -w)、CPU 使用率、队列堆积情况反复调优。

6.2 三组典型参数(补充,可直接抄)

java 复制代码
// ① CPU 密集型:核心数 + 1,有界队列要小,避免任务积压
int cpu = Runtime.getRuntime().availableProcessors();
new ThreadPoolExecutor(cpu + 1, cpu + 1, 0L, TimeUnit.MILLISECONDS,
        new ArrayBlockingQueue<>(1024), new NamedThreadFactory("cpu-pool"),
        new ThreadPoolExecutor.AbortPolicy());

// ② IO 密集型:按 1 + W/C 放大(这里取 2 倍核数起步,靠压测微调)
new ThreadPoolExecutor(cpu * 2, cpu * 4, 60L, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(2048), new NamedThreadFactory("io-pool"),
        new ThreadPoolExecutor.CallerRunsPolicy());

// ③ 定时/延迟任务:用 ScheduledThreadPoolExecutor,别手写 Thread.sleep 轮询
new ScheduledThreadPoolExecutor(2, new NamedThreadFactory("sched-pool"));

NamedThreadFactory(强烈建议自定义,排查问题能一眼看出是哪个池):

java 复制代码
public class NamedThreadFactory implements ThreadFactory {
    private final AtomicInteger seq = new AtomicInteger();
    private final String prefix;
    public NamedThreadFactory(String prefix) { this.prefix = prefix; }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, prefix + "-" + seq.incrementAndGet());
        t.setDaemon(false);
        t.setUncaughtExceptionHandler((th, e) -> {
            System.err.println("[thread-pool] uncaught in " + th.getName());
            e.printStackTrace();
        });
        return t;
    }
}

6.3 动态调参

由于 corePoolSize / maximumPoolSize / keepAliveTime / handler 都能在运行时修改(volatile + setter),结合配置中心(Nacos/Apollo)做热更新是非常常见的实践:

java 复制代码
@Component
public class DynamicThreadPoolManager {
    private final ThreadPoolExecutor executor;

    public DynamicThreadPoolManager() {
        this.executor = new ThreadPoolExecutor(8, 16, 60, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(1024), new NamedThreadFactory("dynamic-pool"),
                new ThreadPoolExecutor.CallerRunsPolicy());
        refresh(8, 16);
    }

    /** 配置中心变更回调 */
    public void refresh(int core, int max) {
        // ① 先定 max(前提 core <= max),② 再设 core ------ 顺序写反会抛 IllegalArgumentException
        if (max >= core) {
            executor.setMaximumPoolSize(max);
            executor.setCorePoolSize(core);
        }
    }
}

动态调参的关键约束(很多人踩坑),对着 JDK 源码看更清楚:

java 复制代码
public void setCorePoolSize(int corePoolSize) {
    if (corePoolSize < 0 || maximumPoolSize < corePoolSize)   // ← 校验相对当前的 max
        throw new IllegalArgumentException();
    int delta = corePoolSize - this.corePoolSize;
    this.corePoolSize = corePoolSize;
    if (workerCountOf(ctl.get()) > corePoolSize)
        interruptIdleWorkers();      // 线程多了:唤醒空闲线程,让它们在 getTask 里退出
    else if (delta > 0) {
        // 线程要多少"没人知道",按队列现有任务数预热,队列空了就停
        int k = Math.min(delta, workQueue.size());
        while (k-- > 0 && addWorker(null, true)) {
            if (workQueue.isEmpty()) break;
        }
    }
}

public void setMaximumPoolSize(int maximumPoolSize) {
    if (maximumPoolSize <= 0 || maximumPoolSize < corePoolSize)  // ← 必须 >= 当前 core
        throw new IllegalArgumentException();
    this.maximumPoolSize = maximumPoolSize;
    if (workerCountOf(ctl.get()) > maximumPoolSize)
        interruptIdleWorkers();      // 线程数超了新上限:多出来的会被回收
}
  1. 顺序 :必须先 setMaximumPoolSize 再 setCorePoolSize 。因为 setCorePoolSize 校验的是"当前的 maximumPoolSize",如果先调大 core 而 max 还是旧的小值,直接抛 IllegalArgumentException。反过来"整体缩容"(core、max 都变小)时,先设 core(此时 max 还是大的,合法),再设 max。一句话记:先把上限撑开,再设下限。
  2. 改小 core 不会立刻杀线程 :setCorePoolSize 只是 interruptIdleWorkers() 唤醒空闲线程,再由 getTask 里的 timed 判断(wc > corePoolSize)让它们自然退出,正在跑任务的线程会把手上的活干完。
  3. 调大 core 时会按队列长度预热 :Math.min(delta, workQueue.size()) 个新线程,队列空了就提前停止,避免白白创建空闲线程。
  4. allowCoreThreadTimeOut(true) 可以让核心线程也"弹性" :适合流量波峰波谷明显的场景,注意 keepAliveTime 必须 > 0。

监控:用钩子 + 统一打点

java 复制代码
new ThreadPoolExecutor(core, max, keepAlive, unit, queue, factory, handler) {
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        if (t != null) { metrics.error("task failed", t); }   // 任务异常计数
    }
    @Override
    protected void beforeExecute(Thread t, Runnable r) { metrics.startTimer(); }
};

另外包一层 RejectedExecutionHandler 统计拒绝次数,定时采集 getPoolSize() / getActiveCount() / getQueue().size() / getLargestPoolSize() / getCompletedTaskCount()。

线上必须盯的指标:poolSize、activeCount、queue.size()、largestPoolSize、completedTaskCount、拒绝次数。队列持续增长 / 拒绝率上升,就意味着该扩容或该检查是不是有慢任务。

更好的选择 :如果允许引依赖,直接上美团的 DynamicTp 或 hippo4j 这类动态线程池框架,内置了监控、告警、配置热更新,省得自己造轮子。


七、避坑:几种容易出事的"顺手"写法

这部分是最容易被面试官问、也最容易在线上出事的地方。

7.1 不要直接 execute(new Thread(runnable))

网上很多示例会写成 executorService.execute(new Thread(() -> {...}))。这样写:

  • 线程池只是拿着一个 Thread 对象在"执行"它 ,真正的业务跑在 new Thread 自己的线程里;
  • 线程池的参数(线程数、队列)基本失效,等于每个任务都新建线程,池化完全白费;
  • 异常也在那个新线程里抛出,线程池的 UncaughtExceptionHandler 抓不到。

正确写法永远是 executor.execute(() -> {...}) 或 executor.submit(...)。

7.2 submit 会吃掉异常

submit() 返回 Future,任务里抛的异常被包进 Future,只有调用 future.get() 才会抛出来。用 submit 却不 get,异常就"人间蒸发"了。要么用 execute,要么每次 submit 都记得处理 Future。

7.3 线程池里的 ThreadLocal 不会自动清理

线程复用意味着 ThreadLocal 会跟着线程一直存在,任务结束前务必 remove(),否则脏数据会串到下一个任务,甚至OOM。

7.4 记得关闭线程池

shutdown()(优雅)/ shutdownNow()(立即)+ awaitTermination()。Spring 环境里可以注册 @PreDestroy 或 DisposableBean,或者用 Spring 的 ThreadPoolTaskExecutor(destroy() 会内部调 shutdown)。

7.5 任务要响应中断

shutdownNow() 只是 interrupt(),任务里如果是 while (true) { ... } 不检查中断,就永远停不下来。阻塞操作优先用可中断 API(put/take/poll/Lock.lockInterruptibly)。


八、一图回顾 + 高频面试题速答

一次提交任务的完整路径

scss 复制代码
execute(cmd)
 ├─ workerCount < corePoolSize ──► addWorker(cmd, true) ──► new Worker + t.start() ──► runWorker
 ├─ isRunning && queue.offer(cmd) ──► recheck:关闭了→remove+reject;线程数0→addWorker(null,false)
 └─ else addWorker(cmd, false) 失败 ──► reject(cmd) ──► Abort / CallerRuns / Discard / DiscardOldest / 自定义

runWorker: while ((task = getTask()) != null) { lock(); task.run(); unlock(); }
getTask: (take | poll(keepAliveTime)) → 空且超时 → return null → 线程结束回收
shutdown: SHUTDOWN + 中断空闲 worker + 消费完队列 → TIDYING → TERMINATED
问题 要点
线程池为什么能复用线程? runWorker 里的 while 循环 + getTask() 阻塞取任务,任务结束不退出线程
核心线程会回收吗? 默认不回收;allowCoreThreadTimeOut(true) 后按 keepAliveTime 回收
为什么 maximumPoolSize 经常不生效? 队列无界(如默认 LinkedBlockingQueue)时永远 offer 成功,走不到扩容
为什么要 recheck? 入队与 shutdown / 线程退出存在竞态,需要撤销入队或补建线程
线程抛异常了池子会怎样? completedAbruptly=true → processWorkerExit 补建线程;但 submit 会吞异常
shutdown 和 shutdownNow 区别? SHUTDOWN 执行完队列 vs STOP 中断 + 丢弃队列并返回
为什么用 ctl 一个变量? 一次 CAS 同时更新状态+线程数,无锁且高效

九、结语

线程池的本质其实就一句话:用上限可控的线程集合,配合一个阻塞队列,把"不定量、突发式"的任务变成"稳定、可观测"的执行流。 源码里所有的技巧------ctl 位运算、双层自旋 CAS、Worker 内置不可重入锁、recheck 二次检查------都是围绕"并发下的正确性 + 低开销"做的取舍。

最后三条实践建议:

  1. 生产禁用 Executors ,手写 ThreadPoolExecutor,队列必须有界,线程必须命名;
  2. 核心池、最大池、队列容量三者的容量关系要画出来算清楚 ,再做压测。在任务持续提交(未执行完)的瞬间,最大可接收任务数 = corePoolSize + queue.capacity + (maximumPoolSize - corePoolSize) ,超过即拒绝。以 core=2, max=4, ArrayBlockingQueue(2) 为例,最多只能吃下 2 + 2 + 2 = 6 个任务,第 7 个就开始抛 RejectedExecutionException。(注:若有任务在这期间执行完毕,后续任务可以继续提交,所以这给的是"瞬时容量"。)
  3. 上监控:队列长度、活跃线程数、拒绝率是线程池的三大生命体征,必要上动态调参。

有什么问题欢迎大家评论区留言!!!

相关推荐
考研保研资料分享1 小时前
生态学保研经验:从中央民族大学到上交,科研分工与多校考核(2025)
面试
Wang's Blog1 小时前
Java 项目实战: 交友项目 - 个人资料管理的需求分析与接口设计
java·开发语言·交友
北风toto1 小时前
Linux 服务器如何判断 CPU 是否支持 x86-64-v2 指令集
java·linux·服务器
萧瑟余晖1 小时前
Spring Security综合实战与安全加固详解
java·安全·spring
imDwAaY1 小时前
操作系统如何管理程序运行?从线程、进程到内存隔离与用户态切换:CS162 Note2
android·java·redis
const_qiu2 小时前
第三章 08:3.10 本章小结
java·人工智能
Pniubi2 小时前
快速排序(力扣基础)(递归)
java·数据结构·算法
CarIise2 小时前
Spring MVC 请求参数绑定与常用注解2
java·spring·mvc
七夜zippoe2 小时前
Spring Boot 接通义千问:从拿 Key 到第一个对话接口跑通(保姆级)
java·spring boot·后端