Why Your ScheduledExecutorService Stops After an Exception—and How to Fix It
A mis‑handled exception in a ScheduledExecutorService can silently halt the entire scheduled thread pool, causing critical periodic jobs to stop; this article explains the underlying delay‑queue and thread‑pool mechanics, shows reproducible code, and provides practical safeguards to keep scheduled tasks alive.
Background
In a production system a ScheduledExecutorService was used to refresh partner data from MySQL every 60 seconds and cache it locally. An exception in the task caused the periodic execution to stop, potentially losing millions of orders.
Problem
The scheduled task queries the database and updates a local cache. If the database call throws, the task fails and the pool appears to hang, never executing subsequent cycles.
Reproducible Example
public class Demo {
private static ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(1);
private List<String> partnerCache = new ArrayList<>();
@PostConstruct
public void init() {
scheduledExecutorService.scheduleAtFixedRate(() -> loadPartner(),
3, 60, TimeUnit.SECONDS);
}
public void loadPartner() {
List<String> partnerList = queryPartners();
partnerCache.clear();
partnerCache.addAll(partnerList);
}
public List<String> queryPartners() {
// Simulated DB failure
throw new RuntimeException();
}
}Running the program prints the first log entry, then stops because the exception prevents further executions.
Key Questions
Why does an exception in a task affect the scheduled thread pool?
Is the thread pool actually dead?
How does ScheduledExecutorService work internally?
Principle Analysis
1. DelayQueue
ScheduledExecutorServiceis built on a DelayQueue (an unbounded BlockingQueue that holds objects implementing Delayed). Elements become available only after their delay expires.
class MyDelayedTask implements Delayed {
private final long start = System.currentTimeMillis(); // creation time
private final long delay; // delay in ms
MyDelayedTask(long delay) { this.delay = delay; }
@Override
public long getDelay(TimeUnit unit) {
return unit.convert((start + delay) - System.currentTimeMillis(),
TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
MyDelayedTask other = (MyDelayedTask) o;
return Long.compare(this.getDelay(TimeUnit.MILLISECONDS),
other.getDelay(TimeUnit.MILLISECONDS));
}
}The queue orders tasks by remaining delay, so the task whose delay expires soonest is at the head.
2. ThreadPool Mechanics
If the current number of threads is below the core size, a new core thread is created.
If the pool size exceeds the core size, new tasks are placed into the work queue.
If the queue is full, non‑core threads are added.
If that also fails, the task is rejected according to the pool’s rejection policy.
3. Scheduled Task Execution
When scheduleAtFixedRate is called, a ScheduledFutureTask is created and inserted into the DelayQueue:
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit) {
ScheduledFutureTask<Void> sft = new ScheduledFutureTask<>(
command, null,
triggerTime(initialDelay, unit),
unit.toNanos(period));
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
private void delayedExecute(RunnableScheduledFuture<?> task) {
super.getQueue().add(task); // put into DelayQueue
ensurePrestart(); // start a core thread if needed
}Worker threads repeatedly take the first element from the DelayQueue and invoke run():
for (;;) {
Runnable r = workQueue.take(); // blocks until delay expires
r.run();
}The run() implementation of ScheduledFutureTask calls runAndReset(). If the user code throws, runAndReset() catches the throwable, marks the task state as EXCEPTIONAL, and returns false. When false is returned, the task does not call setNextRunTime() nor reExecutePeriodic(), so it is not re‑inserted into the queue. The periodic task therefore disappears, while the underlying thread pool remains alive.
Conclusion
The scheduled thread pool itself does not die; only the faulty task is dropped because its exception prevents rescheduling. To keep periodic tasks alive, wrap the task body in a try‑catch block, e.g.:
try {
// task logic
} catch (Throwable t) {
log.error("Scheduled task failed", t);
}Understanding the composition of DelayQueue and thread pool, and handling exceptions inside scheduled jobs, prevents silent outages.
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.
dbaplus Community
Enterprise-level professional community for Database, BigData, and AIOps. Daily original articles, weekly online tech talks, monthly offline salons, and quarterly XCOPS&DAMS conferences—delivered by industry experts.
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.
