java
concurrency
RejectedExecutionException
SingleThreadExecutor
thread-management

What are the possible reason for a java.util.concurrent.RejectedExecutionException in a SingleThreadExecutor

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

A RejectedExecutionException from a single-thread executor means a submitted task was refused because the executor was not willing or able to accept more work. Even though Executors.newSingleThreadExecutor() uses one worker thread and a queue, it still follows standard executor lifecycle rules. In real applications, rejections usually come from shutdown state, lifecycle races, or custom one-thread executors with bounded queues.

What a Single-Thread Executor Guarantees

Executors.newSingleThreadExecutor() runs submitted tasks sequentially on one background thread.

java
1import java.util.concurrent.ExecutorService;
2import java.util.concurrent.Executors;
3
4ExecutorService executor = Executors.newSingleThreadExecutor();
5executor.submit(() -> System.out.println("running"));
6executor.shutdown();

It guarantees order of execution for accepted tasks, but it does not guarantee that submissions will always be accepted.

Most Common Cause: Submission After Shutdown

The most common cause is simple: code submits a task after shutdown() or shutdownNow() has already been called.

java
1import java.util.concurrent.ExecutorService;
2import java.util.concurrent.Executors;
3
4public class Demo {
5    public static void main(String[] args) {
6        ExecutorService executor = Executors.newSingleThreadExecutor();
7        executor.shutdown();
8        executor.submit(() -> System.out.println("rejected"));
9    }
10}

After shutdown begins, the executor may finish already-queued tasks, but it will reject new ones immediately.

Race Conditions Between Components

In larger systems, the issue is often not in one method. One component starts shutdown while another still believes the executor is open.

Typical sequence:

  • A service begins shutdown.
  • Another thread receives one last event.
  • That thread submits background work.
  • The executor rejects because shutdown already started.

This is a lifecycle coordination bug, not an executor implementation bug.

Mutable Executor Ownership Problems

Another cause is replacing executor instances while other code still holds old references.

java
1class TaskRunner {
2    private ExecutorService executor = Executors.newSingleThreadExecutor();
3
4    void restart() {
5        executor.shutdown();
6        executor = Executors.newSingleThreadExecutor();
7    }
8}

If another component still submits to the old instance, it will see RejectedExecutionException after restart.

Custom One-Thread Executors Can Reject Without Shutdown

Not every one-thread executor is created with newSingleThreadExecutor(). Some systems use ThreadPoolExecutor with one worker and a bounded queue.

java
1import java.util.concurrent.ArrayBlockingQueue;
2import java.util.concurrent.ThreadPoolExecutor;
3import java.util.concurrent.TimeUnit;
4
5ThreadPoolExecutor executor = new ThreadPoolExecutor(
6    1,
7    1,
8    0L,
9    TimeUnit.MILLISECONDS,
10    new ArrayBlockingQueue<>(2)
11);

If tasks arrive faster than the worker can process them and the queue fills up, the executor rejects tasks even though it is not shutting down.

Check State, but Do Not Rely Only on It

You can reduce confusion by checking executor state before submitting:

java
1if (!executor.isShutdown()) {
2    executor.submit(() -> doWork());
3} else {
4    System.out.println("executor closed");
5}

This is useful for logging and fallback behavior, but it is not a complete fix because shutdown can still happen immediately after the check. Strong ownership rules are better than scattered state checks.

Add a Rejection Handler for Visibility

If you use a custom ThreadPoolExecutor, set a rejection handler so failures include more context.

java
executor.setRejectedExecutionHandler((task, ex) -> {
    System.err.println("Task rejected. isShutdown=" + ex.isShutdown());
});

This is much easier to debug than a bare exception with no surrounding lifecycle information.

Test Rejection Paths Explicitly

Do not test only the happy path. Rejection is often part of shutdown and resiliency behavior.

java
1try {
2    executor.shutdown();
3    executor.submit(() -> System.out.println("x"));
4} catch (java.util.concurrent.RejectedExecutionException ex) {
5    System.out.println("rejection observed as expected");
6}

That kind of test is especially useful around shutdown hooks, schedulers, and long-lived service components.

Common Pitfalls

  • Assuming a single-thread executor cannot reject because it has only one worker.
  • Calling shutdown() too early in the application lifecycle.
  • Letting several components own the same executor without a clear contract.
  • Replacing executor instances while old references are still in use.
  • Ignoring rejection behavior in custom bounded-queue executors.

Summary

  • 'RejectedExecutionException means the executor refused a new task.'
  • The usual cause is submission after shutdown.
  • Lifecycle races are common when multiple components share one executor.
  • Custom one-thread executors can also reject because of bounded queues.
  • Clear ownership and explicit shutdown rules are the real solution.

Course illustration
Course illustration

All Rights Reserved.