当前位置: 首页 > news >正文

孤舟笔记 并发篇二十三 线程池是如何实现线程复用的?Worker循环取任务的秘密远比你想象的精巧

文章目录

    • 一、先说结论:线程复用的核心机制
    • 二、Worker:线程和任务的"合体"
    • 三、runWorker():复用的核心循环
    • 四、getTask():从队列取下一个任务
    • 五、完整流程:从提交到复用
    • 六、对比:不使用线程池 vs 使用线程池
    • 线程复用机制全景
    • 回答技巧与点评
        • 标准回答
        • 加分回答
        • 面试官点评

个人网站

每个 Thread 只能 start 一次,那线程池怎么做到"复用"线程的?一个线程执行完任务为什么没有退出?它是怎么拿到下一个任务的?

搞不懂线程复用的原理,线程池对你来说就是黑盒。今天咱们拆开看看。

一、先说结论:线程复用的核心机制

维度说明
核心思路Worker 线程在 while 循环中反复取任务,取到就执行,取不到就等
关键代码runWorker() 中的 while (task != null || (task = getTask()) != null)
Worker 本质继承 AQS,既是线程又是任务载体
首次任务Worker 创建时绑定 firstTask,先执行它
后续任务firstTask 执行完后,从 workQueue 中取
等待机制核心线程用 take() 永久阻塞,非核心线程用 poll() 超时等待

一句话记住:线程复用就像流水线工人——干完一个零件不是下班,而是伸手拿下一个,没零件就等着,永远不会自己走。

二、Worker:线程和任务的"合体"

Worker 是 ThreadPoolExecutor 的内部类,它同时继承了 AQS 并实现了 Runnable:

privatefinalclassWorkerextendsAQSimplementsRunnable{finalThreadthread;// Worker 对应的线程 👈RunnablefirstTask;// 第一个任务(可能为 null) 👈volatilelongcompletedTasks;Worker(RunnablefirstTask){setState(-1);// 禁止在 runWorker 前被中断this.firstTask=firstTask;this.thread=getThreadFactory().newThread(this);// 用自己创建线程 👈}publicvoidrun(){runWorker(this);// 线程启动后执行 runWorker 👈}}

关键:Worker 本身就是 Runnable,thread 的 run() 方法执行的就是 Worker.run() → runWorker()。线程启动后,就进入了 runWorker 的循环。

三、runWorker():复用的核心循环

finalvoidrunWorker(Workerw){Threadwt=Thread.currentThread();Runnabletask=w.firstTask;// 先拿第一个任务 👈w.firstTask=null;w.unlock();// 允许被中断try{while(task!=null||(task=getTask())!=null){// 👈 核心循环w.lock();// 执行任务时加锁(防止 shutdown 同时中断)// 检查线程池状态if((runStateAtLeast(ctl.get(),STOP)||(Thread.interrupted()&&runStateAtLeast(ctl.get(),STOP)))&&!wt.isInterrupted())wt.interrupt();try{beforeExecute(wt,task);// 钩子方法try{task.run();// 👈 执行任务!}catch(Throwablex){afterExecute(task,x);// 钩子方法throwx;}finally{afterExecute(task,null);// 钩子方法}}finally{task=null;// 清空,下一轮从队列取 👈w.completedTasks++;w.unlock();}}}finally{processWorkerExit(w,false);// 线程退出 👈}}

复用的秘密全在 while 循环:

  1. 首次:执行 Worker 绑定的 firstTask
  2. 后续:task = getTask()从队列取
  3. 取到了 → 执行 → task 置 null → 下一轮继续取
  4. 取不到 → 循环退出 → 线程终止

线程没有退出,是因为 while 循环没结束。这就是复用的本质。

四、getTask():从队列取下一个任务

privateRunnablegetTask(){booleantimedOut=false;for(;;){// ... 状态检查 ...booleantimed=allowCoreThreadTimeOut||wc>corePoolSize;try{Runnabler=timed?workQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS):// 限时等 👈workQueue.take();// 永久等 👈if(r!=null)returnr;timedOut=true;}catch(InterruptedExceptionretry){timedOut=false;}}}
场景取任务方式行为
核心线程take()队列空时永久阻塞,直到有任务
非核心线程poll(keepAliveTime)队列空时等待 keepAliveTime,超时返回 null
队列有任务poll/take 立即返回拿到任务继续执行

生活类比:核心线程是正式员工,在工位上一直等(take);非核心线程是临时工,等了 keepAliveTime 还没事干就下班了(poll 超时)。

五、完整流程:从提交到复用

提交任务 submit(task) │ ├─ workerCount < corePoolSize? │ └─ 是 → addWorker(task, true) → 创建 Worker,绑定 firstTask 👈 │ ├─ workQueue.offer(task)? │ └─ 是 → 任务入队,等待空闲 Worker 来取 │ └─ 否 → addWorker(task, false) → 创建非核心 Worker Worker 生命周期 │ start() → run() → runWorker() │ while 循环: ├─ 执行 firstTask(第一次) ├─ getTask() 从队列取(后续) 👈 复用的关键 ├─ 取到 → task.run() ├─ 取不到 → 循环退出 → processWorkerExit() └─ 线程终止

六、对比:不使用线程池 vs 使用线程池

// 不用线程池:每个任务一个线程,执行完就销毁for(inti=0;i<1000;i++){newThread(task).start();// 创建1000个线程,用完即销毁 👈}// 线程池:10个线程循环执行1000个任务ExecutorServicepool=Executors.newFixedThreadPool(10);for(inti=0;i<1000;i++){pool.submit(task);// 10个Worker循环取任务,复用线程 👈}

节省了 990 个线程的创建和销毁开销。

线程复用机制全景

线程复用 全景 核心原理 Worker 线程在 while 循环中反复取任务 ├── firstTask → 首次执行 ├── getTask() → 后续从队列取 └── 循环退出 → 线程终止(不再复用) 关键方法 ├── runWorker() ── while 循环取任务并执行 ├── getTask() ── 从 workQueue 获取任务 │ ├── take() ── 核心线程永久等 │ └── poll(timeout) ── 非核心线程限时等 └── processWorkerExit() ── 线程退出善后 Worker 设计 ├── extends AQS ── 实现不可重入锁(标记线程是否在执行任务) ├── implements Runnable ── 本身是可执行体 └── thread = new Thread(this) ── 用自己创建线程 口诀:Worker 是循环工,firstTask 先干完, getTask 从队列取,取到就干取不到等, 核心永远等,非核心限时等, while 循环不停歇,线程复用自然成。

回答技巧与点评

标准回答

线程池通过 Worker 线程的 while 循环实现线程复用。Worker 继承 AQS 并实现 Runnable,创建时绑定一个 firstTask。线程启动后执行 runWorker(),在 while 循环中先执行 firstTask,然后不断通过 getTask() 从 workQueue 获取下一个任务。核心线程使用 take() 永久阻塞等待,非核心线程使用 poll(keepAliveTime) 限时等待。只要 getTask() 能取到任务,线程就不会退出,从而实现复用。

加分回答
  1. Worker 的 AQS 锁设计:Worker 继承 AQS 实现了不可重入的互斥锁,用于标记线程是否正在执行任务。shutdown() 时只中断空闲线程(锁未被占用),不中断正在执行任务的线程。不可重入是为了防止任务代码中调用 shutdown() 时误判线程状态
  2. beforeExecute/afterExecute 钩子:runWorker() 在任务执行前后留了钩子方法,子类可以覆盖来实现监控、统计、日志等功能。这是模板方法模式的体现
  3. 线程复用 vs 对象池:线程池的复用不是"对象池"那种借还模式,而是"循环取任务"模式。线程始终持有运行权,只是在不同任务之间切换。这种设计更高效,避免了线程的频繁创建和上下文切换
面试官点评

这道题考的是你对线程池内部运作机制的深入理解。能说出"while 循环取任务"是核心,但能讲清 Worker 的设计(AQS 锁、firstTask、getTask 的区别)、核心线程和非核心线程的不同等待策略、以及 beforeExecute/afterExecute 钩子的作用,才说明你真的读过源码。面试官最想听到的核心认知是:线程复用的本质不是"线程被回收再分配",而是"线程一直在循环中跑,只是执行的任务在变"。

原文阅读


内容有帮助?点赞、收藏、关注三连!评论区等你 💪

http://www.jsqmd.com/news/750396/

相关文章:

  • 2026支付宝立减金回收攻略:过期作废太可惜,这样操作轻松换额度 - 可可收
  • FOCUS方法:解决多主体图像生成中的属性绑定与空间关系问题
  • 语言如何刻写自感:从黄玉顺“生活存在论”到“痕迹政治学”的元重释
  • PyTorch模型保存的两种方式(.pth全量 vs state_dict),哪种更适合转ONNX?一次讲清楚
  • Obsidian Excel插件:构建企业级知识库结构化数据管理的完整方案
  • 从寄存器操作到库函数:我的ZYNQ OV5640+LCD显示工程优化与重构心得
  • 为 OpenClaw Agent 工作流配置 Taotoken 作为统一的模型提供商
  • 终极解决方案:如何用OBS多平台推流插件实现一次编码多平台直播
  • 内网部署音频AI项目,我踩遍了librosa、numba和llvmlite的版本坑(附完整依赖清单)
  • 惠阳中大型塑胶模胚加工及代表性厂家 - 昌晖模胚
  • 告别HX711!用STM32和CS1238搭建低成本高精度电子秤方案(附完整工程)
  • 告别SDK卡顿!ZYNQ-7020上两种HDMI图片显示方案的实战对比与选择
  • OneDrive同步总出bug?程序员教你用Git思维来管理和排查同步问题
  • 多模态AI策略内化技术:提升对话系统理解与执行能力
  • 如何快速打造智能机器狗:openDogV2开源四足机器人完整指南
  • Hive事务表从入门到放弃?手把手教你配置ACID表并避坑(基于ORC存储)
  • Translumo:打破语言障碍的实时屏幕翻译利器
  • VTR开源EDA工具链:从Verilog到布线的完整流程与优化实战
  • 2026 大连黄金回收避坑指南:选福正美,不扣点不熔金 - 福正美黄金回收
  • 学术论文一键转交互网页的技术实现与应用
  • 通过 Taotoken CLI 工具一键配置开发环境与常用工具
  • 批量自动化任务里,为什么节流和间隔控制不能省
  • Mediapipe姿态估计避坑指南:解决Windows/Mac环境配置、摄像头延迟和关键点抖动
  • Claude Code 接入 DeepSeek-V4-Pro
  • Spark SQL执行计划保姆级解读:从Parsed到Physical,手把手教你用explain(mode=‘extended‘)
  • 显卡驱动深度清理指南:Display Driver Uninstaller (DDU) 一站式解决方案
  • YOLO系列算法改进 | C2PSA改进篇 | 融合HEWL高频增强小波层 | 频域引导与边缘细节增强,适应红外弱小目标与边缘部署场景 | TGRS 2026
  • 告别Oracle,拥抱PostgreSQL:用Navicat迁移数据时,我踩过的那些坑和最佳实践
  • 5分钟解锁:LinkSwift网盘直链解析的终极效率秘籍
  • Visdom蓝屏?可能是你的‘环境’没选对!深入理解PyTorch+Visdom环境隔离机制