🚀 引言:为什么要手写线程池?
在日常开发中,我们习惯了Executors.newFixedThreadPool()或者直接new ThreadPoolExecutor()。但你是否思考过:
- 为什么线程池能让线程“长生不老”而不被销毁?
- 一个
int变量是如何神奇地同时表示“线程数量”和“运行状态”的? - 当任务洪峰来临时,线程池内部的微秒级调度逻辑是怎样的?
今天,我们摒弃那些繁琐的参数定义,直接动刀,从零实现一个具备核心调度、任务队列、拒绝策略功能的工业级线程池。
一、 核心设计:线程池的“灵魂”状态机
线程池不是简单的线程集合,而是一个复杂的状态机。在 JDK 中,作者 Doug Lea 巧妙地使用了一个AtomicInteger(32位)来存储两个信息:
- 高 3 位:表示线程池运行状态(RUNNING, SHUTDOWN, STOP…)。
- 低 29 位:表示当前有效线程数。
1.1 状态转换逻辑流
二、 第一阶段:构建核心骨架与 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)时,线程池会经历三个关键判断:
- 核心线程数(corePoolSize):没满?直接创线程执行。
- 阻塞队列(workQueue):满了?尝试放进队列。
- 最大线程数(maximumPoolSize):队列也满了?尝试开启非核心线程。
- 拒绝策略(Handler):全满了?触发拒绝。
3.1 调度流程图(Mermaid 渲染)
四、 第三阶段:极致实现——自定义拒绝策略
当系统负载达到极限,如何体面地拒绝任务?我们需要提供一个接口:
publicinterfaceRejectedExecutionHandler{voidrejectedExecution(Runnabler,MyThreadPoolExecutorexecutor);}// 模拟 JDK 的 DiscardOldestPolicy(丢弃最老任务策略)publicclassDiscardOldestPolicyimplementsRejectedExecutionHandler{publicvoidrejectedExecution(Runnabler,MyThreadPoolExecutorexecutor){executor.getQueue().poll();// 弹出队首executor.execute(r);// 重新尝试提交}}五、 隐形陷阱:手写线程池时的 3 个坑
5.1 变量可见性与原子性
在增加线程计数时,必须使用AtomicInteger的compareAndSet(CAS),否则在高并发下,线程数会超出你的限制,直接撑爆内存。
5.2 线程工厂(ThreadFactory)的重要性
不要在代码里硬编码new Thread()。手写时,一定要支持传入ThreadFactory,以便为线程命名。没有名字的线程池,排查 OOM 时就是噩梦。
5.3 锁的颗粒度
在维护HashSet<Worker>(保存存活线程的容器)时,必须加全局锁(如ReentrantLock),这决定了线程池的线程安全性。
六、 总结:从手写到架构思考
通过手写,你会发现ThreadPoolExecutor的伟大之处在于它对资源的极致克制。它利用CAS + 阻塞队列,在无锁化和稳定性之间取得了近乎完美的平衡。
架构师建议:生产环境严禁使用Executors的快捷方法,一定要手动配置参数,并根据业务是CPU 密集型还是IO 密集型来计算核心线程数。
七、 互动引导
如果你在面试中遇到这道题:
“如果线程池的队列满了,但核心线程都在忙,新来的任务会发生什么?”
你能精准地回答出 addWorker 的执行时机吗?
欢迎在评论区留下你的代码实现片段。点赞过百,我将开源这套手写的“极简版 ThreadPoolExecutor”完整工程代码(包含单元测试)!