Why Does ScheduledExecutorService Spike CPU to 100%? Uncovering a JDK Bug
This article explains how a JDK bug in ScheduledExecutorService can cause a thread pool with coreSize 0 to enter a tight loop, driving CPU usage to 100 %, and shows the fixes introduced in JDK 9 as well as a related bug in scheduleWithFixedDelay.
The author discovers a bug in ScheduledExecutorService where configuring the core pool size to 0 (due to a static setCoreSize method) makes the thread pool consume 100% CPU.
Demo
A simple demo creates a scheduled thread pool with core size 0 and schedules a task that prints "业务逻辑" after 60 seconds.
public static void main(String[] args){
ScheduledExecutorService e = Executors.newScheduledThreadPool(0);
e.schedule(() -> {
System.out.println("业务逻辑");
}, 60, TimeUnit.SECONDS);
e.shutdown();
}Although the code runs and prints after the delay, the JVM spins in a tight loop, pushing CPU usage to 100%.
What’s happening?
The root cause is a JDK bug (JDK‑8065320) where ThreadPoolExecutor#getTask repeatedly calls workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) when corePoolSize is 0 and keepAliveTime is also 0. Because keepAliveTime is zero, the poll never blocks, causing an endless busy‑wait.
ScheduledExecutorService executor = Executors.newScheduledThreadPool(0);
When the pool has no tasks, the core thread should block, but with keepAliveTime = 0 it continuously polls, leading to 100% CPU.
How JDK 9 fixed it
In JDK 9 the default keep‑alive value was changed from 0 ns to DEFAULT_KEEPALIVE_MILLIS = 10L (10 ms). This prevents the busy loop when corePoolSize is 0.
Another related bug
A second bug (JDK‑8051859) occurs in scheduleWithFixedDelay when the delay is extremely large (e.g., Long.MAX_VALUE). Converting the negative delay to nanoseconds overflows to Long.MIN_VALUE, causing the next run time to be far in the past (about 292 years ago).
public class ScheduledTaskBug {
public static void main(String[] args) throws InterruptedException {
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleWithFixedDelay(() -> System.out.println("running" + new java.util.Date()),
0, Long.MAX_VALUE, TimeUnit.MICROSECONDS);
executor.submit(() -> System.out.println("immediate" + new java.util.Date()));
Thread.sleep(5000);
executor.shutdownNow();
}
}The overflow makes the calculated trigger time a distant past, so the task never runs again.
The fix is to compute the nanoseconds as -unit.toNanos(delay) instead of unit.toNanos(-delay), avoiding the overflow.
Summary
Both bugs stem from edge‑case values (core size 0, zero keep‑alive, extreme delays) that trigger infinite loops or overflow in the JDK’s thread‑pool implementation. Updating to JDK 9 or later, or manually setting a non‑zero keep‑alive, resolves the CPU‑spike issue, while correcting the delay conversion eliminates the scheduleWithFixedDelay overflow.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
