- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
你好呀,我是歪歪. 给大家分享一个关于 ScheduledExecutorService 线程池的 BUG 啊,这个 BUG 能直接把 CPU 给飚到 100%,希望大家永远踩不到. 但是,u1s1,一般来说也很难踩到. 到底咋回事呢,让我给你细细嗦嗦. 老规矩,按照惯例,先搞个 Demo 出来玩玩: 项目里面使用到了 ScheduledThreadPoolExecutor 线程池,该线程池对应的核心线程数放在配置文件里面,通过 @Value 注解来读取配置文件. 然后通过接口触发这个线程池里面的任务. 具体来说就是在上面的示例代码中,在调用 testScheduledPool 接口之后,程序会在 60 秒之后输出“执行业务逻辑”. 这个代码的逻辑还是非常简单清晰的,但是上面的代码有一个问题,不知道你看出来没有? 没看出来也没关系,我这里都是鼓励式教学的,不打击同学的积极性. 所以,别着急,我先给你跑起来,你瞅一眼立马就能看出问题是啥: 为什么 coreSize 是 0 呢,我们配置文件里面明明写的是 2 啊? 因为 setCoreSize 方法是 static 的,导致 @Value 注解失效. 如果去掉 static 那么就能正确读取到配置文件中的配置: 虽然这里面也大有学问,但是这并不是本文的重点,这只是一个引子, 。 为的是引出为什么会在项目里面出现下面这种 coreSize 等于 0 的奇怪的代码: ScheduledExecutorService executor = Executors.newScheduledThreadPool(0), 如果我直接给出上面的代码,一点有人说只有小(大)可(傻)爱(逼)才会这样写. 但是铺垫一个背景之后,就容易接受的多了. 你永远可以相信我的行文结构,老司开车稳得很,你放心. 好,经过前面的铺垫,其实我们的 Demo 能直接简化到这个样子: 这个代码是可以正常运行的,你粘过去直接就能跑,60 秒后是会正常输出的. 如果你觉得 60 秒太长了,那么你可以改成 3 秒运行一下,看看程序是不是正常运行并结束了. 但是就这个看起来问题不大的代码,会导致 CPU 飚到 100% 去. 真的,儿豁嘛. 到底咋回事呢? 这个其实就是 JDK 的 BUG 导致的,我带你瞅瞅: https://bugs.openjdk.org/browse/JDK-8065320 。 首先,你看 Fix Version 那个地方是 9,也就是说明这个 BUG 是在 JDK 9 里面修复了。JDK 8 里面是可以复现的. 其次,这个标题其实就包含了非常多的信息了,它说对于 ScheduledExecutorService 来说 getTask 方法里面存在频繁的循环. 那么问题就来了:频繁的循环,比如 for(;;) ,while(true) 这样的代码,长时间从循环里面走不出来,会导致什么现象? 那不就是导致 CPU 飙高吗. 注意,这里我说的是“长时间从循环里面走不出来”,而不是死循环,这两者之间的差异还是很大的. 我代码里面的示例就是使用的提出 BUG 的哥们给出的实例: 他说,在这个示例下,如果你在一个只有单核的服务器上跑,然后使用 TOP 命令,会看到持续 60 秒的 CPU 使用率为 100%. 为什么呢? 答案就藏在前面提到的 getTask 方法中: java.util.concurrent.ThreadPoolExecutor#getTask 。 这个方法里面果然是有一个类似于无线循环的代码,但是它为什么不停的执行呢? 现在赶紧想一想线程池的基本运行原理。当没有任务处理的时候,核心线程在干啥? 是不是就阻塞在这个地方,等着任务过来进行处理的,这个能理解吧: 那我再问你一个问题,这行代码的作用是干啥: workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) 。 是不是在指定时间内如果没有从队列里面拉取到任务,则抛出 InterruptedException. 那么它什么时候会被触发呢? 在 timed 参数为 true 的时候. timed 参数什么时候会为 true 呢? 当 allowCoreThreadTimeOut 为 true 或者当前工作的线程大于核心线程数的时候. 而 allowCoreThreadTimeOut 默认为 false: 那么也就是在这个案例下满足了当前工作的线程大于核心线程数这个条件: wc > corePoolSize 。 通过 Debug 知道,wc 是 1,corePoolSize 为 0: 所以 timed 变成了 true. 好,这里要注意了,朋友. 经过前面的分析,我们已经知道了在当前的案例下,会触发 for(;;)这个逻辑: workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) 。 那么这个 keepAliveTime 到底是多少呢? 来,大声的喊出这个数字:0,这是一个意想不到的、诡计多端的 0. 所以,这个地方中的 r 每次都会返回一个 null,然后再次开启循环. 对于正常的线程池来说,触发了这个逻辑,代表没有任务要执行了,可以把对应线程进行回收了. 回收,对应的就是这部分代码会返回一个 null: 然后在外面 runWorker 方法中的,由于 getTask 返回了 null,从而执行了 finally 代码里面的逻辑,也就是从当前线程池移除线程的逻辑: 但是,朋友,我要说但是了. 在我们的案例下,你看 if 判断的条件: 这里面的 wc > 1 || workQueue.isEmpty()) 是 false 。 所以这个 if 条件不成立,那么它又走到了 poll 这里: workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) 。 由于这里的 keepAliveTime 是 0,所以马不停蹄的的开启下一轮循环. 那么这个循环什么时候结束呢? 就是在从队列里面获取到任务的时候. 那么队列里面什么时候才会有任务呢? 在我们的案例里面,是 60 秒之后. 所以,在这 60 秒内,这部分代码相当于是一个“死循环”,导致 CPU 持续飙高到 100%. 这就是 BUG,这就是根本原因. 但是看到这里是不是觉得还差点意思? 我说 100% 就 100% 吗? 得拿出石锤来才行啊. 所以,为了拿出实锤,眼见为实,我把核心流程拿出来,然后稍微改动一点点代码: AffinityLock 这个类在之前的文章里面出现过: 《面试官:Java如何绑定线程到指定CPU上执行?》 。 就是把线程绑定到指定 CPU 上去执行,减少 CPU 抖动带来的损耗, 具体我就不介绍了,有兴趣去看我之前的文章. 把这个程序跑起来之后,打开资源监视器,你可以看到 5 号 CPU 立马就飚到 100% 了,停止运行之后,立马就下来了: 这就是眼见为实,这真是 JDK 的 BUG,我真没骗你. 在 JDK 9 里面是怎么修复这个 BUG 的呢? 在前面提到的 BUG 的链接中,有这样的一个链接,里面就是 JDK 9 版本里面针对上述的 BUG 进行的修复: http://hg.openjdk.java.net/jdk9/jdk9/jdk/rev/6dd59c01f011 。 点开这个链接之后,你可以找到这个地方: 首先对比一下标号为 ① 和 ② 的地方,默认值从 0 纳秒修改为了 DEFAULT_KEEPALIVE_MILLIS 毫秒. 而 DEFAULT_KEEPALIVE_MILLIS 的值为在标号为 ③ 的地方, 10L. 也就是默认从 0 纳秒修改为了 10 毫秒。而这一处的改动,就是为了防止出现 coreSize 为 0 的情况. 我们重点关注一下 DEFAULT_KEEPALIVE_MILLIS 上面的那一坨注释. 我给你翻译一下,大概是这样的: 这个值呢一般来说是用不上的,因为在 ScheduledThreadPoolExecutor 线程池里面的线程都是核心线程. 但是,如果用户创建的线程池的时候,不听劝,头铁,要把 corePoolSize 设置为 0 会发生什么呢? 因为 keepAlive 参数设置的为 0,那么就会导致线程在 getTask 方法里面非常频繁的循环,从而导致 CPU 飙高. 那怎么办呢? 很简单,设置一个小而非零的值就可以,而这个小是相对于 JVM 的运行时间而言的. 所以这个 10 毫秒就是这样来的. 在研究前面提到的编号为 8065320 的 BUG 的时候,我还发现一个意外收获,编号为 8051859 的 BUG,它们是挨着的,排排坐. 有点意思,也很简单,所以分享一波: https://bugs.openjdk.org/browse/JDK-8051859 。 这个 BUG 又说的是啥事儿呢: 看截图这个 BUG 也是在 JDK 9 版本之后修复的. 这个 BUG 的标题说的是 ScheduledExecutorService 线程池的 scheduleWithFixedDelay 方法,遇到大延迟时会执行失败. 具体啥意思呢? 我们还是先拿着 Demo 说: 你把这个代码粘过去之后,发现输出是这样的: 只有第一个任务执行了,第二个任务没有输出结果. 正常来说,第一个任务的延迟时间,也就是 initialDelay 参数是 0,所以第一次执行的时候是立即执行: 比如我改成这样,把周期执行的时间单位,由微秒修改为纳秒,就正常了: 神奇不神奇?你说这不是 BUG 这是啥? 提出 BUG 的这个哥们在描述里面介绍了 BUG 的原因,主要是提到了一个字段和两个方法: 一个字段是指 period,两个方法分别是 TimeUnit.toNanos(-delay) 和 ScheduledFutureTask.setNextRunTime(). 首先,在 ScheduledThreadPoolExecutor 里面 period 字段有三个取值范围: 比如我们的示例代码中调用的是 scheduleWithFixedDelay 方法,它里面就会在调用 TimeUnit.toNanos 方法的时候取反,让 period 字段为负数: 好,此时我们开始 Debug 我们的 Demo,先来一个正常的. 比如我们来一个每 30ms 执行一次的周期任务,请仔细看: 在执行 TimeUnit.toNanos(-delay) 这一行代码的时候,把 30 微秒转化为了 -30000 纳秒,也就是把 period 设置为 -30000. 然后来到 setNextRunTime 方法的时候,计算任务下一次触发时间的时候,又把 period 转为正数,没有任何毛病: 但是,当我们把 30 修改为 Long.MAX_VALUE 的时候,有意思的事情就出现了: delay=9223372036854775807 -delay=-9223372036854775807 unit.toNanos(-delay)=-9223372036854775808 。 直接给干溢出了,变成了 Long.MIN_VALUE: 当来到 setNextRunTime 方法的时候,你会发现由于我们的 p 已经是 Long.MIN_VALUE 了. 那么 -p 是多少呢? 给你跑一下: Long.MIN_VALUE 的绝对值,还是 Long.MIN_VALUE。一个神奇的小知识点送给你,不客气. 所以 -p 还是 Long.MIN_VALUE: 我们来算一下啊,一秒等于 10 亿纳秒: 那么下一次触发时间就变成了这样: 292 年之前. 这就是在 BUG 描述中提到的: This results in triggerTime returning a time in the distant past. 。 the distant past,就是 long long ago,就是 292 年之前。就是 1731 年,雍正九年,那个时候的皇帝还是九子夺嫡中一顿乱杀,冲出重围的胤禛大佬. 确实是很久很久以前了. 那么这个 BUG 怎么修复呢? 其实很简单: 把 unit.toNanos(-delay) 修改为 -unit.toNanos(delay),搞定. 我给你盘一下: 这样就不会溢出,时间就变成了 292 年之后. 那么问题就来了,谁特么会设置一个每 292 年执行一次的 Java 定时任务呢? 好,你看到这里了,本文就算结束了,我来问你一个问题:你知道了这两个 BUG 之后,对你来说有什么收获吗? 没有,是的,除了浪费了几分钟时间外,没有任何收获. 那么恭喜你,又在我这里学到了两个没有卵用的知识点. 这个小节为什么叫做汇总呢? 因为我发现这里出现的一串 BUG,除了本文提到的 2 个外,还有 3 个我都写过了,所以在这里汇个总,充点字数,凑个篇幅: 8054446: Repeated offer and remove on ConcurrentLinkedQueue lead to an OutOfMemoryError 。 《我的程序跑了60多小时,就是为了让你看一眼JDK的BUG导致的内存泄漏。》 。 这篇文章就是从 ConcurrentLinkedQueue 队列的一个 BUG 讲起。jetty 框架里面的线程池用到了这个队列,导致了内存泄漏. 同时通过 jconsole、VisualVM、jmc 这三个可视化监控工具,让你看见“内存泄漏”的发生. 8062841: ConcurrentHashMap.computeIfAbsent stuck in an endless loop 。 《震惊!ConcurrentHashMap里面也有死循环,作者留下的“彩蛋”了解一下?》 。 这个 BUG 在 Dubbo 和 Seata 里面都有提到过,也被 Seata 官方的一篇博客中被引用过: https://seata.io/zh-cn/blog/seata-dsproxy-deadlock.html 。 8073704: FutureTask.isDone returns true when task has not yet completed 。 《Doug Lea在J.U.C包里面写的BUG又被网友发现了。》 。 这个 BUG 也是在 JDK 9 版本里面修复的,逻辑弯弯绕绕的,但是理解之后,对于 FutureTask 状态流转就能有一个比较深刻的认知了。有兴趣可以看看. Demo
public static void main(String[] args){
ScheduledExecutorService e = Executors.newScheduledThreadPool(0);
e.schedule(() -> {
System.out.println(
"业务逻辑"
);
}, 60, TimeUnit.SECONDS);
e.shutdown();
}
咋回事呢
public static void main(String[] args) {
ArrayBlockingQueue<Runnable> workQueue =
new ArrayBlockingQueue<>(100);
//绑定到 5 号 CPU 上执行
try (AffinityLock affinityLock = AffinityLock.acquireLock(5)) {
for
(; ; ) {
try {
Runnable r = workQueue.poll(0, TimeUnit.NANOSECONDS);
if
(r != null)
break
;
} catch (InterruptedException retry) {
}
}
}
}
怎么修复
再来一个
public class ScheduledTaskBug {
static public void main(String[] args) throws InterruptedException {
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
//第一个任务
executor.scheduleWithFixedDelay(new
Runnable
() {
@Override
public void
run
() {
System.out.println(
"running scheduled task with delay: "
+ new Date());
}
}, 0, Long.MAX_VALUE, TimeUnit.MICROSECONDS);
//第二个任务
executor.submit(new
Runnable
() {
@Override
public void
run
() {
System.out.println(
"running immediate task: "
+ new Date());
}
});
Thread.sleep(5000);
executor.shutdownNow();
}
}
汇总
最后此篇关于刺激,线程池的一个BUG直接把CPU干到100%了。的文章就讲到这里了,如果你想了解更多关于刺激,线程池的一个BUG直接把CPU干到100%了。的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我想知道在谈到 CPU 使用率和 CPU 利用率时,术语是否存在科学差异。我觉得这两个词都被用作同义词。它们都描述了 CPU 时间和 CPU 容量之间的关系。 Wikipedia称之为 CPU 使用率
我研究了一些关于处理器和 Tomasulo 算法的指令重新排序的内容。 为了更深入地了解这个主题,我想知道是否有任何方法可以(获取跟踪)查看为给定程序完成的实际动态重新排序? 我想给出一个输入程序并查
我有一台配备 2 个 Intel Xeon CPU E5-2620 (Sandy Bridge) 和 10Gbps 82599 NIC(2 个端口)的服务器,用于高性能计算。从 PCI 关联性中,我看
您能详细解释一下“用户 CPU 时间”和“系统 CPU 时间”吗?我读了很多,但我不太理解。 最佳答案 区别在于时间花在用户空间还是内核空间。用户 CPU 时间是处理器运行程序代码(或库中的代码)所花
我想知道如何识别 CPU 是否与 ARM v5 指令集兼容。 假设 ARM v7 指令与 ARM v5 兼容是否正确? 最佳答案 您可以阅读 CPUID base register获得PARTNO。然
我目前在具有多个六核 CPU 的服务器上使用 C 多线程。我想将我的一些线程的亲和性设置为单个 CPU 的各个核心。我使用过 pthread_setaffinity_np() 和 sched_seta
1) 独占时间是在方法中花费的时间2) 包含时间是在方法中花费的时间加上在任何被调用函数中花费的时间3)我们称调用方法为“ parent ”,称方法为“子”。引用链接:Click here 这里的问题
关闭。这个问题需要多问focused 。目前不接受答案。 想要改进此问题吗?更新问题,使其仅关注一个问题 editing this post . 已关闭 5 年前。 Improve this ques
好的,所以编译器可以出于性能原因自由地重新排序代码片段。让我们假设一些代码片段,在没有应用优化的情况下直接翻译成机器代码,看起来像这样: machine_instruction_1 machine_i
我在 zabbix 中有以下默认图表,但我不知道如何解释这些值。谁能解释一下? 最佳答案 操作系统是一件非常忙碌的事情,尤其是当你让它做某事时(即使你没有做)。当我们看到一个活跃的企业环境时,总会发生
换句话说,L1、L2、L3 等缓存是否总是反射(reflect) CPU的字节序 ? 或者总是将数据存储在某些 的缓存中更有意义吗?特定字节序 ? 有没有总体设计决策 ? 最佳答案 大多数现代缓存不会
我想知道当前的 cpus 是否避免在其中至少一个为零时将两个数字相乘。谢谢 最佳答案 这取决于 CPU 和(在某些情况下)操作数的类型。 较旧/较简单的 CPU 通常使用如下乘法算法: integer
我有一个 CUDA 应用程序,它在一台计算机(配备 GTX 275)上运行良好,而在另一台配备 GeForce 8400 的计算机上运行速度慢了大约 100 倍。我怀疑有某种回退使代码实际上在 CPU
例如,对于 8 位 CPU,堆栈大小预计为 8 位宽,16 位 CPU 与 16 位堆栈宽度,以及 32 位、64 位 CPU,等等。是否适用于所有架构? 最佳答案 CPU 具有数据总线和地址总线。它
实现 SIMD 是否需要多核 CPU? 在阅读有关 SIMD 的维基百科时,我发现了以下短语“多处理元素”。那么这句话和“多核CPU”有什么区别呢? 最佳答案 不,每个内核通常都可以执行指令集中的大多
我遗漏了一些基本的东西。 CPU 流水线:在基本层面上,为什么指令需要不同数量的时钟周期才能完成,为什么有些指令在多级 CPU 中只需要 1 个周期? 除了明显的“不同的指令需要不同的工作量才能完成”
超线程 CPU 是实现并行还是仅实现并发(上下文切换)? 我的猜测是没有并行性,只有通过上下文切换的并发性。 最佳答案 单个物理 CPU 具有超线程的核心显示为 两个逻辑 CPU 到操作系统。 CPU
关闭。这个问题不符合Stack Overflow guidelines .它目前不接受答案。 这个问题似乎不是关于 a specific programming problem, a softwar
背景是这样的:下周我们的办公室将有一天因为维护而没有暖气。预计室外温度在 7 至 12 摄氏度之间,因此可能会变冷。可移植电取暖器数量太少,无法满足所有人的需求。 但是,在我大约 6-8 平方米的办公
我开发了一个应用程序,该应用程序在我的开发箱上的三个容器中运行,该开发箱具有带超线程的四核,这意味着系统和 docker 使用 8 个核心。 容器的 CPU 分配由 docker-compose 完成
我是一名优秀的程序员,十分优秀!