news 2026/6/10 11:16:25

线程池简单源码思路手撕实现和关于参数设置

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
线程池简单源码思路手撕实现和关于参数设置

线程池简单源码思路手撕实现

importjava.util.ArrayList;importjava.util.List;importjava.util.concurrent.BlockingQueue;importjava.util.concurrent.TimeUnit;publicclassmyThreadPool{privateintcorePoolSize;privateintmaxPoolSize;privateinttimeout;privateTimeUnittimeUnit;publicBlockingQueue<Runnable>blockingQueue;privateRejectHandlerejectHandle;privateList<Thread>coreList=newArrayList<>();privateList<Thread>supportList=newArrayList<>();publicmyThreadPool(intcorePoolSize,intmaxPoolSize,inttimeout,TimeUnittimeUnit,BlockingQueue<Runnable>blockingQueue,RejectHandlerejectHandle){this.corePoolSize=corePoolSize;this.maxPoolSize=maxPoolSize;this.timeout=timeout;this.timeUnit=timeUnit;this.blockingQueue=blockingQueue;this.rejectHandle=rejectHandle;}voidexecute(Runnablecommand){if(coreList.size()<corePoolSize){CoreThreadthread=newCoreThread(command);coreList.add(thread);thread.start();return;}if(blockingQueue.offer(command)){return;}if(coreList.size()+supportList.size()<maxPoolSize){SupportThreadthread=newSupportThread(command);supportList.add(thread);thread.start();return;}if(!blockingQueue.offer(command)){rejectHandle.reject(command,this);}}classCoreThreadextendsThread{privatefinalRunnablefirstTask;CoreThread(RunnablefirstTask){this.firstTask=firstTask;}@Overridepublicvoidrun(){firstTask.run();while(true){try{Runnablecommand=blockingQueue.take();command.run();}catch(InterruptedExceptione){thrownewRuntimeException(e);}}}}classSupportThreadextendsThread{privatefinalRunnablefirstTask;SupportThread(RunnablefirstTask){this.firstTask=firstTask;}@Overridepublicvoidrun(){firstTask.run();while(true){try{Runnablecommand=blockingQueue.poll(timeout,timeUnit);if(command==null){break;}command.run();}catch(InterruptedExceptione){thrownewRuntimeException(e);}System.out.println(Thread.currentThread().getName()+"线程死掉了");supportList.remove(Thread.currentThread());}}}}
首先是重要的线程池参数:

核心线程数:定义不会被回收的核心线程数量

最大线程数:定义除了核心线程以外的临时线程,临时线程超过时间还没任务就会被销毁

阻塞队列:选择用什么阻塞队列,使用阻塞队列是防止在核心/辅助线程再去拿任务的时候一直while对cpu空转的损害

拒绝策略:在超过最大线程就会采取对应的拒绝策略-比如报错舍弃/静默丢弃/舍弃旧任务(或者也可以考虑持久化到mysql保存起来)

时间:这个是辅助线程没任务的存活时间。

时间单位:这个是辅助线程没任务的存活时间的单位。

线程工厂:用来决定线程的命名,创建线程的方式,管理线程优先级之类的。这里只是简单demo所以没有实现。

voidexecute(Runnablecommand){if(coreList.size()<corePoolSize){CoreThreadthread=newCoreThread(command);coreList.add(thread);thread.start();return;}if(blockingQueue.offer(command)){return;}if(coreList.size()+supportList.size()<maxPoolSize){SupportThreadthread=newSupportThread(command);supportList.add(thread);thread.start();return;}if(!blockingQueue.offer(command)){rejectHandle.reject(command,this);}}
这个是重要的线程池接受任务的过程:

核心线程没满就用核心线程,满了就先放到阻塞队列,阻塞队列满了就创建临时辅助线程,辅助线程也满了就考虑拒绝策略。

classCoreThreadextendsThread{privatefinalRunnablefirstTask;CoreThread(RunnablefirstTask){this.firstTask=firstTask;}@Overridepublicvoidrun(){firstTask.run();while(true){try{Runnablecommand=blockingQueue.take();command.run();}catch(InterruptedExceptione){thrownewRuntimeException(e);}}}}classSupportThreadextendsThread{privatefinalRunnablefirstTask;SupportThread(RunnablefirstTask){this.firstTask=firstTask;}@Overridepublicvoidrun(){firstTask.run();while(true){try{Runnablecommand=blockingQueue.poll(timeout,timeUnit);if(command==null){break;}command.run();}catch(InterruptedExceptione){thrownewRuntimeException(e);}System.out.println(Thread.currentThread().getName()+"线程死掉了");supportList.remove(Thread.currentThread());}}}}

这里有firsttask的任务主要是在线程刚创建就来执行,减少再去阻塞队列拿的麻烦,核心线程和辅助线程从阻塞队列拿任务的方法分别是take()和poll()主要是后者限制时间,辅助线程执行完就会被销毁这里是发生在run()执行完。而take()永久等待出不来就不会被销毁。

然后看一下拒绝策略
packagetech.insight;/** * @author gongxuanzhangmelt@gmail.com **/publicinterfaceRejectHandle{voidreject(RunnablerejectCommand,MyThreadPoolthreadPool);}
packagetech.insight;/** * @author gongxuanzhangmelt@gmail.com **/publicclassThrowRejectHandleimplementsRejectHandle{@Overridepublicvoidreject(RunnablerejectCommand,MyThreadPoolthreadPool){thrownewRuntimeException("阻塞队列满了!");}}
packagetech.insight;/** * @author gongxuanzhangmelt@gmail.com **/publicclassDiscardRejectHandleimplementsRejectHandle{@Overridepublicvoidreject(RunnablerejectCommand,MyThreadPoolthreadPool){threadPool.blockingQueue.poll();threadPool.execute(rejectCommand);}}

只实现了报错丢弃和丢弃旧任务加入新任务。

importjava.util.concurrent.ArrayBlockingQueue;importjava.util.concurrent.TimeUnit;publicclassMain{publicstaticvoidmain(String[]args){myThreadPool myThreadPool=newmyThreadPool(2,10,1,TimeUnit.SECONDS,newArrayBlockingQueue<>(2),newThrowRejectHandle());for(inti=0;i<10;i++){finalintfi=i;myThreadPool.execute(()->{try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}System.out.println(Thread.currentThread().getName()+" "+fi);});}}}

main里初始化线程池,以及添加10个任务,以上就是线程池的简单实现。

接下来我记录一些自己看的扩展

关于参数该怎么设置?比如核心线程数和最大线程数和其他的?

线程数

首先看八股怎么说:

cpu密集型:核心线程数=cpu核心数加减1,最大线程数=cpu核心数加减1

io密集型:核心线程数=cpu核心数*2,最大线程数=CPU核心数 × 4 或更高

然后还有具体的场景:

比如在电商场景:流量波动极大。平时可能没什么人,一旦活动开启,流量瞬间暴涨。可以极端的把核心线程数设置为0(还是留一点但是占比小),全是临时线程没有任务就回收。

还有记录日志场景:这种一般没什么变化,就全部设置为核心线程数就好,最大线程数等于核心线程数。

阻塞队列

阻塞队列选有界的如ArrayBlockingQueueLinkedBlockingQueue(n),因为无界队列存在内存溢出风险,还有**SynchronousQueue:** 之前提到的电商核心线程为 0 时常用。它不存任务,直接“手递手”交给线程。

关于存活时间

电商/高并发场景:建议设置得稍长一些(如 60s)。因为流量往往是波动性的,频繁地销毁和重新创建线程是非常消耗 CPU 的。

后台低频任务:建议设置得短一些(如 1s 或 10s)。任务处理完赶紧释放资源。

默认参考:Java 自带的CachedThreadPool默认是60s,这是一个非常合理的平衡点点。

可以理解为核心线程少就长一点,多就短一点。

拒绝策略

当线程池满了(最大线程已开,队列也塞满了),新来的任务怎么办?Java 提供了四种标准策略:

  1. AbortPolicy(默认):直接抛出RejectedExecutionException
    • 适用:关键业务,必须让调用方知道任务失败了。
  2. CallerRunsPolicy(调用者运行):谁提交的任务,谁自己去执行。
    • 适用:最重要的兜底方案。它会减慢生产者的速度(因为生产者去干活了,没空发任务了),起到一种天然的“降级”和“限流”作用。
  3. DiscardPolicy:直接丢掉任务,不报任何错。
    • 适用:比如打印无关紧要的日志、监控埋点,丢了就丢了。
  4. DiscardOldestPolicy:丢弃队列里排队最久(最老)的任务,把当前任务塞进去。
    • 适用:具有“时效性”的任务,比如实时行情数据,旧的数据没意义了。

根绝业务性质来选择拒绝策略。

还有美团的动态线程池是怎么搞的?

先讲讲为什么有这个需求?

所谓的“美团动态线程池”,核心本质就是:配置中心(如 Nacos/Apollo) + JDK 线程池原生 API + 监控告警

它不是创造了一种新的线程池技术,而是一套管理和运维线程池的架构方案。这个概念最早源于美团技术团队发表的一篇经典文章《Java线程池实现原理及其在美团业务中的实践》,后来成为了业界标准。

传统痛点:线程池参数(核心数、最大数、队列长度)通常写死在代码或配置文件里。一旦上线,发现流量太大导致队列积压,或者参数设置不合理导致 CPU 飙升,必须改代码 -> 重新打包 -> 重启服务。这在“双11”这种分秒必争的时刻是致命的。

动态线程池能做到什么:

热更新:像开关灯一样,运营/开发在后台修改参数,服务端的线程池瞬间生效,无需重启

全景监控:实时看到线程池在干嘛(队列堆了多少、当前活跃线程多少)。

具体是怎么实现的?

它的实现并不复杂,主要分为三步:管理、监听、修改

第一步:管理(注册中心)

也就是你要把系统里所有的线程池都管理起来。通常会做一个ThreadPoolManager,用一个Map把所有线程池存起来,每个线程池有个名字(比如order-pool,log-pool)。

第二步:监听(配置中心)

这是关键。利用Nacos、Apollo、Etcd等配置中心的能力。

  • 你在 Nacos 上修改配置:order-pool.coreSize = 20
  • 应用程序里的监听器(Listener)捕捉到配置变化。(只要改了主动推送)

第三步:修改(JDK API - 很多人不知道的秘密)

这是最核心的。很多人以为线程池创建了就不能改,其实JDK 的ThreadPoolExecutor原生就提供了 public 的 set 方法

这里还有一个点,默认的阻塞队列是final的想要修改长度只能自己实现阻塞队列并去掉final。

还要配合定时任务去监控读取运行时指标保证安全,也要监控队列使用率比如超过80%报警。

省流:

“其实参数设置没有绝对的标准,核心准则是:即不要让 CPU 闲置(吞吐量太低),也不要让队列积压导致 OOM(内存溢出)。

所以,动态线程池才是最终的解决方案。因为线上流量是未知的,我们无法在写代码时就预测出最完美的参数,只有具备了动态调整可视化监控的能力,才能让系统在面对突发流量时立于不败之地。”

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

NPP 草原:南非图文巴,1949-1990 年,R1

NPP Grassland: Towoomba, South Africa, 1949-1990, R1 简介 本数据集包含七个文本格式 (.txt) 的数据文件。这些文件提供了在南非图文巴人工建立的草原稀树草原研究地点进行的生物量估算、土壤碳 (C)、氮 (N) 和磷 (P) 测量数据。该研究地点是长期施肥试验的一部分&#xf…

作者头像 李华
网站建设 2026/4/22 9:02:53

Windows Server 2016 中文版、英文版下载 (2026 年 1 月更新)

Windows Server 2016 中文版、英文版下载 (2026 年 1 月更新) Windows Server 2016 x64 Version 1607 (updated Jan 2026) 请访问原文链接&#xff1a;https://sysin.org/blog/windows-server-2016/ 查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;…

作者头像 李华
网站建设 2026/6/2 12:03:17

BMP280气压传感器原理图设计,已量产(压力传感器)

目录 1、电源系统:低噪声是精准感知的基础 2、BMP280 核心电路:不拖传感器性能的后腿 3、电平转换:5V/3.3V 主控的兼容关键 4、硬件兼容性:不用跳线的 “傻瓜式” 适配 做嵌入式项目时,“小而精” 的传感器模块永远是刚需 —— 尤其是需要同时测气压和温度的场景,比如…

作者头像 李华
网站建设 2026/5/26 15:22:27

开发基于大模型的金融专业教材章节总结生成器

开发基于大模型的金融专业教材章节总结生成器 关键词:大模型、金融专业教材、章节总结生成器、自然语言处理、文本生成 摘要:本文旨在详细阐述开发基于大模型的金融专业教材章节总结生成器的全过程。从背景介绍入手,深入探讨核心概念、算法原理、数学模型等内容,通过项目实…

作者头像 李华
网站建设 2026/6/2 18:19:05

什么是SQL注入

文章目录SQL注入原理SQL注入类型华为WAF5000-Web应用防火墙SQL注入是一种代码注入技术&#xff0c;也是最危险的Web应用程序漏洞之一。攻击者在用户输入字段中插入恶意代码&#xff0c;欺骗数据库执行SQL命令&#xff0c;从而窃取、篡改或破坏各类敏感数据。业界常用Web应用防火…

作者头像 李华