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.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
Why Does ScheduledExecutorService Spike CPU to 100%? Uncovering a JDK Bug

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ThreadPoolJava concurrencyScheduledExecutorServiceJDK bugCPU Spike
Su San Talks Tech
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.