news 2026/4/15 20:31:57

线程池竟能把CPU干到100%?两个JDK BUG踩坑实录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
线程池竟能把CPU干到100%?两个JDK BUG踩坑实录
  • 线程池竟能把CPU干到100%?两个JDK BUG踩坑实录
    • 一、CPU 100%惨案:核心线程数为0引发的血案
      • 1. 为什么会CPU飙满?
      • 2. JDK是怎么修复的?
      • 3. 避坑建议
    • 二、定时任务“失踪”:Long.MAX_VALUE引发的时间溢出
      • 1. 溢出的秘密:负负得不了正
      • 2. 修复方式与避坑
    • 三、总结:JDK BUG不可怕,避坑有方法

线程池竟能把CPU干到100%?两个JDK BUG踩坑实录

做Java开发的都知道,ScheduledExecutorService是日常处理定时任务的常用工具,稳定又可靠。但我最近踩了两个它的隐藏BUG,其中一个直接把CPU飙到100%,另一个让定时任务彻底“罢工”。今天就把这两个坑扒出来,带你看看JDK底层的小猫腻。

一、CPU 100%惨案:核心线程数为0引发的血案

先看一段看似正常的代码,这段代码能直接跑起来,但会让CPU在任务执行前疯狂飙高:

publicstaticvoidmain(String[]args){// 核心线程数设置为0ScheduledExecutorServiceexecutor=Executors.newScheduledThreadPool(0);// 60秒后执行任务executor.schedule(()->{System.out.println("业务逻辑执行完毕");},60,TimeUnit.SECONDS);executor.shutdown();}

你没看错,核心线程数设为0的ScheduledThreadPoolExecutor居然能正常创建,而且会在60秒内把单个CPU核心吃到100%。这不是业务逻辑的问题,而是JDK的原生BUG。

1. 为什么会CPU飙满?

要搞懂这个问题,得钻进线程池的核心方法ThreadPoolExecutor.getTask()里看看。这个方法是线程池获取任务的关键,里面有个无限循环:

privateRunnablegetTask(){booleantimedout=false;for(;;){intc=ctl.get();intrs=runStateOf(c);// 状态判断逻辑省略...intwc=workerCountOf(c);// 判断线程是否需要超时回收booleantimed=allowCoreThreadTimeout||wc>corePoolSize;// 线程回收逻辑省略...try{// 超时获取任务或阻塞获取任务Runnabler=timed?workQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS):workQueue.take();if(r!=null)returnr;timedout=true;}catch(InterruptedExceptionretry){timedout=false;}}}

当核心线程数corePoolSize=0时,会触发两个关键条件:

  • wc > corePoolSize成立(默认会创建1个工作线程,wc=1),所以timed=true
  • ScheduledThreadPoolExecutor的默认keepAliveTime是0纳秒。

这就导致线程会在workQueue.poll(0, NANOSECONDS)处无限循环——每次poll都立即返回null,然后进入下一次循环,全程不阻塞,相当于一个无意义的死循环,直接把CPU干满。

更坑的是,这个循环会一直持续到任务执行的那一刻,比如上面代码中就会持续60秒的CPU 100%占用。

2. JDK是怎么修复的?

这个BUG在JDK 9中被正式修复,修复方式特别简单:把默认keepAliveTime从0纳秒改成了10毫秒。

// JDK 9修复后privatestaticfinallongDEFAULT_KEEPALIVE_MILLIS=10L;publicScheduledThreadPoolExecutor(intcorePoolSize){super(corePoolSize,Integer.MAX_VALUE,DEFAULT_KEEPALIVE_MILLIS,MILLISECONDS,newDelayedWorkQueue());}

10毫秒的超时时间,让线程在没任务时会阻塞10毫秒,而不是无限循环,CPU占用瞬间就降下来了。

3. 避坑建议

  • 不要把ScheduledThreadPoolExecutor的核心线程数设为0,哪怕是配置失误也不行;
  • 如果使用JDK 8及以下版本,创建线程池时手动指定keepAliveTime
    // JDK 8避坑写法ScheduledExecutorServiceexecutor=newScheduledThreadPoolExecutor(0,Executors.defaultThreadFactory(),newThreadPoolExecutor.AbortPolicy()){{setKeepAliveTime(10,TimeUnit.MILLISECONDS);}};

二、定时任务“失踪”:Long.MAX_VALUE引发的时间溢出

除了CPU飙满的BUG,scheduleWithFixedDelay还藏着一个溢出问题。看这段代码:

publicstaticvoidmain(String[]args)throwsInterruptedException{ScheduledExecutorServiceexecutor=Executors.newSingleThreadScheduledExecutor();// 延迟Long.MAX_VALUE微秒执行下一次任务executor.scheduleWithFixedDelay(()->{System.out.println("定时任务执行");},0,Long.MAX_VALUE,TimeUnit.MICROSECONDS);// 提交一个立即执行的任务executor.submit(()->{System.out.println("立即任务执行");});Thread.sleep(5000);executor.shutdownNow();}

运行后你会发现,只有“定时任务执行”输出,“立即任务执行”居然消失了!线程池直接陷入不可用状态。

1. 溢出的秘密:负负得不了正

scheduleWithFixedDelay的底层会把延迟时间存储在period字段中,而且为了区分“固定延迟”和“固定速率”,会把period设为负数:

publicScheduledFuture<?>scheduleWithFixedDelay(Runnablecommand,longinitialDelay,longdelay,TimeUnitunit){// 省略参数校验...ScheduledFutureTask<Void>sft=newScheduledFutureTask<>(command,null,triggerTime(initialDelay,unit),unit.toNanos(-delay));// 延迟时间取反,转为负数// 省略后续逻辑...}

delay=Long.MAX_VALUE且时间单位是微秒时,unit.toNanos(-delay)会发生溢出:

  • -Long.MAX_VALUE-9223372036854775807
  • 微秒转纳秒需要乘以1000,这个数值乘以1000后会超出long的范围,最终变成Long.MIN_VALUE-9223372036854775808)。

后续计算下一次执行时间时,会把period取反:-Long.MIN_VALUE依然是Long.MIN_VALUE(因为Long.MIN_VALUE的绝对值比Long.MAX_VALUE大1)。这就导致任务的下一次执行时间被算成了“292年前”,线程池一直等着这个过去的时间,后续提交的任务全被阻塞。

2. 修复方式与避坑

JDK 9同样修复了这个问题,把unit.toNanos(-delay)改成了-unit.toNanos(delay),先转纳秒再取反,避免溢出:

// JDK 9修复后unit.toNanos(-delay)-unit.toNanos(delay)

日常开发中避坑也简单:

  • 不要给scheduleWithFixedDelay设置Long.MAX_VALUE这么极端的延迟时间;
  • 如果确实需要超长延迟,改用TimeUnit.NANOSECONDS作为时间单位,避免乘法溢出。

三、总结:JDK BUG不可怕,避坑有方法

这两个BUG都暴露了JDK底层的细节漏洞,但只要掌握核心逻辑,就能轻松避开:

  1. 核心线程数不要乱设,ScheduledThreadPoolExecutorcorePoolSize建议至少设为1;
  2. JDK 8及以下版本使用ScheduledThreadPoolExecutor时,手动指定合理的keepAliveTime
  3. 避免使用极端数值作为定时任务的延迟时间,防止数值溢出;
  4. 条件允许的话,升级到JDK 9及以上版本,从根源上规避这些已修复的BUG。

线程池是Java并发的核心工具,看似简单的API背后可能藏着复杂的底层逻辑。遇到问题时多扒扒源码,不仅能解决当下的坑,还能加深对并发机制的理解,何乐而不为?

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

别再乱写了,Controller 层代码这样写才足够规范!

本篇主要要介绍的就是controller层的处理&#xff0c;一个完整的后端请求由4部分组成&#xff1a; 接口地址(也就是URL地址)、 2. 请求方式(一般就是get、set&#xff0c;当然还有put、delete)、 3. 请求数据(request&#xff0c;有head跟body)、 4. 响应数据(response) 本…

作者头像 李华
网站建设 2026/4/5 2:04:57

Claude在AI原生应用中的5大核心优势解析

Claude在AI原生应用中的5大核心优势解析 关键词&#xff1a;Claude大模型、AI原生应用、长上下文处理、安全对齐、多模态交互 摘要&#xff1a;随着AI技术从"工具辅助"向"原生驱动"进化&#xff0c;AI原生应用&#xff08;AI-Native Apps&#xff09;正成为…

作者头像 李华
网站建设 2026/4/9 14:54:46

百度免费上传组件在内网中如何支持大附件的上传?

《一个前端打工人的奇幻外包历险记》 需求分析&#xff1a;这需求是灭霸提的吧&#xff1f; 各位同行大家好&#xff01;我是一名在福建"苟延残喘"的个人前端开发者。最近接了个外包项目&#xff0c;看到需求文档时我的表情是这样的&#xff1a;&#x1f628; → &…

作者头像 李华