
面试官一句「你分析过线程池源码吗」下来很多人能背出五个创建方法却说不出 execute 里 workerCountOf 和 addWorker 到底怎么协作。我啃 ThreadPoolExecutor 时卡在最难缠的 execute → addWorker 流程上ctl 高 3 位和低 29 位怎么切分workQueue.offer 成功后为什么还要 recheckWorker 又偏偏基于 AQS。后来我让 Codex 逐段拆反而被官方 API 额度和切模型的流程卡住。把 Codex 的通道改到 TaoTokenhttps://taotoken.net/?utm_sourcetaotoken_aicg_blog_end之后才顺着 execute 的每一层分支把源码真正读完。这篇仍然以线程池源码为主线同时把 Codex 走 TaoToken 的配置、验证和用量核对放在对应步骤里。1. 让 Codex 接上 TaoToken拆 ThreadPoolExecutor 前的通道准备先不急着读 execute。既然要让 Codex 帮你逐段拆源码得先确认请求能发出去否则你把 60 行代码贴进去它回一个鉴权错误思路就全断了。1.1 在官网创建 API Key打开 TaoToken注册登录后创建一把 API Key。创建后回填配置时统一用YOUR_API_KEY占位真正使用的时候再换成你自己的 Key。模型 ID 以 TaoToken 模型广场的现网列表为准不要填网上流传的旧 ID。创建好 Key 之后再看 Codex 侧怎么接。1.2 ~/.codex/config.toml 指向 https://taotoken.net/apiCodex 的配置在用户目录下的~/.codex/config.toml把model_provider换成 taotokenbase_url填https://taotoken.net/api环境变量名用TAOTOKEN_API_KEY。完整配置如下model_provider taotoken model YOUR_MODEL_ID [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY然后在 shell 里导出环境变量export TAOTOKEN_API_KEYYOUR_API_KEY这里有两个容易踩的位置要说明。第一base_url是https://taotoken.net/api末尾不要补/v1Codex 自己会按 OpenAI 兼容路径拼接补了会多出路径段。第二如果你的 shell 配置了多个环境变量文件确保export那行在你每次启动 codex 前都被执行否则 Codex 读不到 Key。1.3 发一条短消息验证返回配置完成后启动 codex 输入「解释 ThreadPoolExecutor.execute 方法里 ctl 字段的高 3 位与低 29 位如何协作。」只要它正常返回一段能读懂的中文解析就说明 Base URL、API Key、模型 ID 三者已经对齐。如果这一步收到 401先检查环境变量是否真的导出成功如果返回模型相关错误去模型广场确认YOUR_MODEL_ID有没有写对。验证通过后再让它拆完整的 execute 方法后续每轮追问都会稳定走同一通道。2. Executor 结构先认清 ThreadPoolExecutor 在哪个槽位源码读不懂很多时候是不知道每个类站在哪一层。线程池整体可以拆成四层每一层都有自己的职责边界。2.1 Executor 四层职责Executor 接口只有一个execute(Runnable command)它的语义就是「我把任务交给你你负责跑起来」。ExecutorService 在 Executor 之上补齐了生命周期管理shutdown、submit、invokeAll都在这一层。AbstractExecutorService 则把submit、invokeAny这类带返回值的模板逻辑实现好让下层只关心真正的执行细节。到 ThreadPoolExecutor 才落进具体实现线程怎么创建、任务怎么排队、队列满的时候怎么拒绝。面试时被问到「看过线程池源码吗」先把这四层关系说清楚再往下钻 execute会比直接背创建方法显得扎实。Codex 拆这段时也会按这个顺序展开接口定义职责继续往下看字段和构造器。2.2 ctl 用高 3 位和低 29 位装两件事ThreadPoolExecutor 里第一个拦住人的字段是ctl。它是个 AtomicInteger高 3 位表示 runState低 29 位表示 workerCount。把两个状态塞进一个 int是为了让线程池状态和 worker 数量在修改时保持一致CAS 一次只能更新一个 int如果用两个独立字段要保证原子性就得加锁而ctl的设计避免了不必要的锁竞争。runState 一共有五个值RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED。状态值越大越不活跃。RUNNING 时能接收新任务也能处理队列里的任务SHUTDOWN 之后不接新任务但队列里剩的活儿还要干完STOP 连队列任务也不处理并且会中断所有 workerTIDYING 表示任务都清完了正在收尾TERMINATED 是最终结束态。状态行为RUNNING接新任务处理队列任务SHUTDOWN不接新任务继续处理队列STOP不接新任务中断 worker不处理队列TIDYING任务清空准备终止TERMINATED线程池已终止workerCount 就是当前存活的 worker 数量。execute 里调用workerCountOf(c)读取的正是低 29 位runStateOf(c)读取高 3 位这两个工具方法在后续流程里几乎处处都在用。3. 五种创建方法与构造器先看参数再记结论3.1 JDK8 五种方法对应不同的 ThreadPoolExecutor 形态创建线程池JDK8 提供了五个方法。很多答案让人死记「定长、可变、单线程、定时、工作窃取」但更稳的办法是把它们和 ThreadPoolExecutor 构造器参数对应起来。newFixedThreadPool(nThreads)传入 nThreads 作为 corePoolSize也作为 maximumPoolSize队列用无界的 LinkedBlockingQueue这样线程数永远不会超过 nThreads多出来的任务都在队列里排队。newSingleThreadExecutor等价于固定线程数为 1 的版本再包一层 FinalizableDelegatedExecutorService外部就不能把它强转成 ThreadPoolExecutor 去改配置。newCachedThreadPool的 corePoolSize 是 0maximumPoolSize 是 Integer.MAX_VALUE队列用 SynchronousQueue线程空闲超过 60 秒就会被回收适合大量短任务。newScheduledThreadPool返回 ScheduledThreadPoolExecutor支持延迟执行和周期执行内部队列是 DelayedWorkQueue。newWorkStealingPool比较特殊它返回 ForkJoinPool用分治法把任务拆到多个双端队列适合计算密集的大任务。这五种方法里只有 newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor 最终落到 ThreadPoolExecutor 构造器另外两个各自走了 ForkJoinPool 和 ScheduledThreadPoolExecutor。面试时能说出这一层就不是单纯的背 API。3.2 构造器七个参数如何影响 executeThreadPoolExecutor 构造器有七个参数corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。参数校验有两条硬性规则corePoolSize 不能小于 0maximumPoolSize 不能小于等于 0也不能小于 corePoolSizekeepAliveTime 不能小于 0workQueue、threadFactory、handler 不能为 null。违反第一类抛 IllegalArgumentException违反第二类抛 NullPointerException。参数会被原样存进字段然后 execute 的每个分支都要用到workerCountOf(c)和 corePoolSize 比大小决定要不要新建核心线程workQueue.offer决定任务能否进队队列满时用 maximumPoolSize 判断还能不能扩容扩不了容就交给 handler 执行拒绝策略。所以构造器不是简单的字段赋值它直接决定了 execute 后面所有 if/else 的走向。4. execute 主流程workerCountOf 与 offer 的三层分流execute 是线程池最核心的入口。把它的三个分支理顺addWorker、runWorker、Worker 这些后续概念读起来会轻松很多。4.1 第一层workerCount 小于 corePoolSize 就加人execute 先读一次ctlint c ctl.get(); if (workerCountOf(c) corePoolSize) { if (addWorker(command, true)) { return; } c ctl.get(); }workerCountOf(c)取出低 29 位得到当前存活的 worker 数。只要它小于 corePoolSize线程池就认为人手不够直接addWorker(command, true)新建一个线程去执行这个任务。addWorker 的第二个参数 true 表示按 corePoolSize 作为上限false 表示按 maximumPoolSize 判断。addWorker 失败后为什么要重新ctl.get()因为 addWorker 内部可能已经 CAS 增加过 workerCount也可能被别的线程抢先改了状态原来的 c 已经过期后面再判断isRunning(c)会得到错误结果。4.2 第二层任务入队成功后要 recheck如果 worker 数已经到达 corePoolSizeexecute 会尝试把任务放进阻塞队列if (isRunning(c) workQueue.offer(command)) { int recheck ctl.get(); if (!isRunning(recheck) remove(command)) { reject(command); } else if (workerCountOf(recheck) 0) { addWorker(null, false); } }offer成功只代表任务进队了不代表万事大吉。recheck 两次都是拿最新状态做判断一次是 offer 之前用 c 判断线程池还在 RUNNING一次是 offer 之后用 recheck 判断是否发生了 shutdown。如果入队后线程池已经被 shutdown就要把刚入队的任务移除再走拒绝策略。workerCountOf(recheck) 0这个分支容易被忽视。它表示队列里有任务但没有任何 worker 在消费。此时必须addWorker(null, false)补一个线程这个线程的 firstTask 是 null启动后不会直接执行某个具体任务而是进入 runWorker 的循环通过getTask()从队列里取任务。4.3 第三层队列满时按 maximumPoolSize 扩容如果线程池不是 RUNNING或者任务入队失败说明队列已经满了execute 走最后一个分支else if (!addWorker(command, false)) { reject(command); }这次 addWorker 第二个参数传 false意思是允许线程数突破 corePoolSize只要不超过 maximumPoolSize 就行。如果 maximumPoolSize 也已经满了addWorker 返回 false任务只能交给 handler 走拒绝策略。execute 的四层判断可以总结成四句话workerCount 小于 corePoolSize建线程workerCount 到了 corePoolSize 且队列没满入队队列满了但 workerCount 小于 maximumPoolSize继续建线程队列满且 workerCount 到了 maximumPoolSize拒绝任务。5. addWorker 到 runWorkerWorker 为什么基于 AQSexecute 里每次新建线程都调用 addWorker而 addWorker 的逻辑也是面试官深挖的重灾区。5.1 addWorker 的双层循环与 CASaddWorker 开头是带 retry 标签的双层循环。外层循环负责检查线程池状态如果 runState 已经大于等于 SHUTDOWN并且不是「SHUTDOWN 且 firstTask 为 null 且队列非空」这个特例就直接返回 false。特例的含义是线程池在关闭时不允许再接新任务但队列里还有遗留任务时可以创建空 worker 把剩余任务消费完。内层循环检查 workerCount如果 wc 大于等于 CAPACITY或者大于等于 core 参数对应的上限true 时是 corePoolSizefalse 时是 maximumPoolSize返回 false。通过了就用compareAndIncrementWorkerCount把 workerCount 原子地加一。CAS 失败可能是 workerCount 被其他线程改了也可能是线程池状态变了所以失败后要重新读 ctlrunState 变了就 continue retry 回到外层重新来。这里为什么需要 retry 而不是普通 while因为 CAS 失败有两种原因状态变了要重走外层重新判断 rsworkerCount 变了则留在内层重试 CAS用带标签的循环可以把这两条重试路径清晰地区分开。5.2 mainLock 锁住的是 workers 集合CAS 成功后addWorker 创建 Worker 对象。Worker 内部用 ThreadFactory 创建了真正的线程w.thread。接着代码拿到 mainLock 并加锁。锁的必要性在于workers是一个 HashSet本身不是线程安全的多个线程同时 addWorker 就会并发写 HashSet所以必须用 mainLock 统一保护。持有 mainLock 后还要再检查一次状态线程池处于 RUNNING或者处于 SHUTDOWN 且 firstTask 为 null 时才能把 Worker 加进 workers。如果这时发现新创建的线程t.isAlive()返回 true说明线程已经被启动过属于非法状态抛 IllegalThreadStateException。全部通过后把 w 加进 workers更新 largestPoolSize解锁最后t.start()真正启动线程。5.3 Worker 为什么不用 ReentrantLockWorker 继承 AbstractQueuedSynchronizer同时实现 Runnable。它本质上是一个「可被锁住的任务执行单元」。每次要执行任务时Worker 会把自己的 AQS state 从 0 改成 1表示正在执行任务任务结束后再改回 0表示空闲。关键问题是为什么不用 ReentrantLockReentrantLock 允许同一个线程多次加锁而 AQS 的tryAcquire默认不允许重入。对于 Worker 来说不重入恰好是优点一个线程正在执行任务时它不能再给自己加锁state 保持为 1等任务结束state 回到 0线程才允许被打断。Worker 只需要两个状态独占锁表示正在执行任务不加锁表示空闲。ReentrantLock 的重入能力在这里反而是多余的。5.4 runWorker 循环与 shutdown 的竞态Worker 的 run 方法最终调用runWorker(this)。runWorker 的核心是一个 while 循环while (task ! null || (task getTask()) ! null) { w.lock(); if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() runStateAtLeast(ctl.get(), STOP))) !wt.isInterrupted()) { wt.interrupt(); } try { beforeExecute(wt, task); task.run(); } finally { task null; w.completedTasks; w.unlock(); } } processWorkerExit(w, completedAbruptly);每次拿到任务后先w.lock()任务执行完在 finally 里解锁。这个锁的意义在于和 shutdown 方法形成互斥shutdown 会遍历所有 worker 并 interrupt但 interrupt 要结合锁的状态才能对正在执行任务的线程生效。如果 Worker 正在执行任务锁被占住shutdown 的 interrupt 只能等本轮任务结束如果 Worker 正空闲锁没被占住shutdown 就能立刻打断它让它从 getTask() 返回 null然后退出 while 循环。getTask() 从 workQueue 里取任务。shutdown 之后 getTask 会返回 nullworker 就会走到 processWorkerExit把自己从 workers 集合里移除。shutdown 与 getTask 的竞态就发生在「getTask 即将返回任务」和「shutdown 正在设置中断标记」之间w.lock()的存在保证任务执行和中断在时序上不至于错乱。6. 跑通一次源码讲解并核对 Token 消耗6.1 把 execute 到 runWorker 的提示词发给 Codex通道配置好后给 Codex 发这样一段提示词请按四层拆解 ThreadPoolExecutor.execute 1. workerCountOf(c) 如何从 ctl 中取出低 29 位 2. addWorker(command, true) 在哪些情况下返回 false 3. workQueue.offer 成功后 recheck 要解决什么竞态 4. addWorker(null, false) 中 firstTask 为 null 的作用。 要求每层先给结论再给对应源码行号最后补充一个生产环境例子。Codex 返回后把它输出的结论和你自己理解的 execute 流程对照。如果它提到 shutdown 与 getTask 的竞态你可以接着问「Worker 的 tryAcquire 为什么不允许重入」它会从 ReentrantLock 的可重入和 AQS 的不可重入展开对比。这样一轮对话就能把 execute、addWorker、Worker 基于 AQS 三个难点串起来。6.2 回到 TaoToken 看 Token 消耗这次对话会真实消耗 Token。登录 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 在用量页面能看到本次请求的模型、输入 Token、输出 Token 和总消耗。对照自己发送的源码长度就能估算每一轮追问大约花多少 Token。如果发现当前模型回答源码问题不够细致也可以在同一控制台换模型再试不需要重新配置 Codex。6.3 高频考点自查清单线程池源码相关面试题几乎围绕五个点展开五种创建方法分别对应什么线程池形态线程池五个状态和状态值的大小关系execute 的完整分流流程runWorker 从队列取任务的循环Worker 基于 AQS 解决什么问题。前两个用表格或代码注释就能背下来后三个需要能够口头画流程。面试官的问题是「你分析过线程池源码吗」。下次开口前先打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建 Key把 Codex 的 base_url 指到 https://taotoken.net/api然后从 execute 第一行开始拆。等你看到控制台里这次对话消耗的 Token 时线程池的 execute、addWorker、Worker 基于 AQS 也差不多真的成了你自己的知识。