news 2026/4/16 11:15:08

手写 Java 线程池:从状态转换到拒绝策略的极致实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写 Java 线程池:从状态转换到拒绝策略的极致实现

🚀 引言:为什么要手写线程池?

在日常开发中,我们习惯了Executors.newFixedThreadPool()或者直接new ThreadPoolExecutor()。但你是否思考过:

  • 为什么线程池能让线程“长生不老”而不被销毁?
  • 一个int变量是如何神奇地同时表示“线程数量”和“运行状态”的?
  • 当任务洪峰来临时,线程池内部的微秒级调度逻辑是怎样的?

今天,我们摒弃那些繁琐的参数定义,直接动刀,从零实现一个具备核心调度、任务队列、拒绝策略功能的工业级线程池。


一、 核心设计:线程池的“灵魂”状态机

线程池不是简单的线程集合,而是一个复杂的状态机。在 JDK 中,作者 Doug Lea 巧妙地使用了一个AtomicInteger(32位)来存储两个信息:

  1. 高 3 位:表示线程池运行状态(RUNNING, SHUTDOWN, STOP…)。
  2. 低 29 位:表示当前有效线程数。

1.1 状态转换逻辑流

创建并初始化

调用 shutdown()

调用 shutdownNow()

任务清空

队列 & 线程池全空

线程池全空

terminated() 执行完毕

RUNNING

SHUTDOWN

STOP

TIDYING

TERMINATED


二、 第一阶段:构建核心骨架与 Worker 模型

线程池的核心在于Worker(工作者)。它本质上是一个不断从BlockingQueue中获取任务并执行的循环体。

2.1 任务承载:Worker 内部类实现

privatefinalclassWorkerimplementsRunnable{Threadthread;RunnablefirstTask;Worker(RunnablefirstTask){this.firstTask=firstTask;this.thread=newThread(this);// 真正执行的线程}@Overridepublicvoidrun(){runWorker(this);}}

2.2 核心循环:让线程“复用”的秘密

finalvoidrunWorker(Workerw){Runnabletask=w.firstTask;w.firstTask=null;try{// 【核心代码】如果任务不为空,或者从队列中能取到任务,就一直循环while(task!=null||(task=getTask())!=null){try{task.run();// 执行真正的业务逻辑}finally{task=null;}}}finally{processWorkerExit(w);// 线程退出后的清理}}

三、 第二阶段:调度逻辑——execute方法的精妙推演

当我们调用execute(runnable)时,线程池会经历三个关键判断:

  1. 核心线程数(corePoolSize):没满?直接创线程执行。
  2. 阻塞队列(workQueue):满了?尝试放进队列。
  3. 最大线程数(maximumPoolSize):队列也满了?尝试开启非核心线程。
  4. 拒绝策略(Handler):全满了?触发拒绝。

3.1 调度流程图(Mermaid 渲染)

成功

失败

提交任务 execute

当前线程数 < corePoolSize?

addWorker: 创建核心线程

workQueue.offer: 尝试入队

等待 Worker 调度

当前线程数 < maximumPoolSize?

addWorker: 创建非核心线程

执行拒绝策略 RejectedExecutionHandler


四、 第三阶段:极致实现——自定义拒绝策略

当系统负载达到极限,如何体面地拒绝任务?我们需要提供一个接口:

publicinterfaceRejectedExecutionHandler{voidrejectedExecution(Runnabler,MyThreadPoolExecutorexecutor);}// 模拟 JDK 的 DiscardOldestPolicy(丢弃最老任务策略)publicclassDiscardOldestPolicyimplementsRejectedExecutionHandler{publicvoidrejectedExecution(Runnabler,MyThreadPoolExecutorexecutor){executor.getQueue().poll();// 弹出队首executor.execute(r);// 重新尝试提交}}

五、 隐形陷阱:手写线程池时的 3 个坑

5.1 变量可见性与原子性

在增加线程计数时,必须使用AtomicIntegercompareAndSet(CAS),否则在高并发下,线程数会超出你的限制,直接撑爆内存。

5.2 线程工厂(ThreadFactory)的重要性

不要在代码里硬编码new Thread()。手写时,一定要支持传入ThreadFactory,以便为线程命名。没有名字的线程池,排查 OOM 时就是噩梦。

5.3 锁的颗粒度

在维护HashSet<Worker>(保存存活线程的容器)时,必须加全局锁(如ReentrantLock),这决定了线程池的线程安全性。


六、 总结:从手写到架构思考

通过手写,你会发现ThreadPoolExecutor的伟大之处在于它对资源的极致克制。它利用CAS + 阻塞队列,在无锁化和稳定性之间取得了近乎完美的平衡。

架构师建议:生产环境严禁使用Executors的快捷方法,一定要手动配置参数,并根据业务是CPU 密集型还是IO 密集型来计算核心线程数。


七、 互动引导

如果你在面试中遇到这道题:

“如果线程池的队列满了,但核心线程都在忙,新来的任务会发生什么?”

你能精准地回答出 addWorker 的执行时机吗?

欢迎在评论区留下你的代码实现片段。点赞过百,我将开源这套手写的“极简版 ThreadPoolExecutor”完整工程代码(包含单元测试)!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 13:40:32

Tidb 运维常用命令

常用运维命令# 建用户和对库授权 CREATE USER xx_dev_user% IDENTIFIED BY Dev2026; GRANT ALL PRIVILEGES ON dj_dev.* TO xx_dev_user%; -- schema&#xff1a;dj_dev## 授权所有库权限 -- 2. 授予正确权限 GRANT ALL PRIVILEGES ON *.* TO xx_test_user% WITH GRANT OPTION;…

作者头像 李华
网站建设 2026/4/16 16:29:10

导师推荐!AI论文工具 千笔·专业学术智能体 VS 笔捷Ai,本科生写作神器!

随着人工智能技术的迅猛迭代与普及&#xff0c;AI辅助写作工具已逐步渗透到高校学术写作场景中&#xff0c;成为本科生完成毕业论文不可或缺的辅助手段。越来越多的学生在面对繁重的写作任务时&#xff0c;开始依赖各类AI工具来提升效率、简化流程。然而&#xff0c;市场上AI写…

作者头像 李华
网站建设 2026/4/16 14:32:51

【Linux命令大全】008.磁盘维护之sfdisk命令(实操篇)

【Linux命令大全】008.磁盘维护之sfdisk命令&#xff08;实操篇&#xff09; ✨ 本文为Linux系统磁盘维护命令的全面汇总与深度优化&#xff0c;结合图标、结构化排版与实用技巧&#xff0c;专为高级用户和系统管理员打造。 (关注不迷路哈&#xff01;&#xff01;&#xff01;…

作者头像 李华
网站建设 2026/4/11 7:29:05

javascript之break和continue

javascript的break语句是中断这次循环&#xff0c;循环不再进行 比如计算1-20的累加&#xff0c;当累加值到66时停止计算&#xff0c;输出累加值和当前整数值 let sum0for(let i1;i<20;i){sumsumiif(sum>66){console.log("当前的整数:"i)break}}console.log(…

作者头像 李华