data-driven-docs

Living documentation for evolving technologies

View on GitHub

Executors

Advanced Java 8+ Est. Time: 15min


Table of Contents


Overview

The Executor framework provides a higher-level replacement for working with threads directly. Instead of manually creating threads with new Thread(runnable).start(), the Executor framework manages thread pools, task queuing, and thread lifecycle.

Key Benefits:

The java.util.concurrent.Executors class provides factory methods for creating different types of executor services.

Back to top


ExecutorService Interface

The ExecutorService extends the Executor interface and provides methods for managing task execution and lifecycle.

Core Methods:

// Submit tasks
Future<?> submit(Runnable task);
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);

// Bulk operations
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks);
<T> T invokeAny(Collection<? extends Callable<T>> tasks);

// Lifecycle management
void shutdown();              // Graceful shutdown
List<Runnable> shutdownNow(); // Immediate shutdown
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit);

Back to top


Thread Pool Types

Fixed Thread Pool

Creates a thread pool with a fixed number of threads. Tasks are queued if all threads are busy.

When to use:

ExecutorService executor = Executors.newFixedThreadPool(4);

// Submit tasks
for (int i = 0; i < 10; i++) {
    int taskId = i;
    executor.submit(() -> {
        System.out.println("Task " + taskId + " executed by " +
                          Thread.currentThread().getName());
        // Simulate work
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });
}

// Shutdown gracefully
executor.shutdown();
try {
    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
        executor.shutdownNow();
    }
} catch (InterruptedException e) {
    executor.shutdownNow();
    Thread.currentThread().interrupt();
}

Output: Only 4 threads will execute tasks concurrently, remaining tasks wait in queue.

Back to top

Cached Thread Pool

Creates a thread pool that creates new threads as needed and reuses previously constructed threads when available.

When to use:

ExecutorService executor = Executors.newCachedThreadPool();

// Submit 100 quick tasks
for (int i = 0; i < 100; i++) {
    int taskId = i;
    executor.submit(() -> {
        System.out.println("Quick task " + taskId + " on " +
                          Thread.currentThread().getName());
    });
}

executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);

Characteristics:

Back to top

Single Thread Executor

Creates an executor that uses a single worker thread. Tasks execute sequentially in order.

When to use:

ExecutorService executor = Executors.newSingleThreadExecutor();

// These execute one at a time, in order
executor.submit(() -> System.out.println("Task 1"));
executor.submit(() -> System.out.println("Task 2"));
executor.submit(() -> System.out.println("Task 3"));

executor.shutdown();

Use cases:

Back to top

Scheduled Thread Pool

Creates a thread pool that can schedule commands to run after a delay or execute periodically.

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);

// Execute once after 5 seconds delay
scheduler.schedule(() -> {
    System.out.println("Executed after 5 seconds");
}, 5, TimeUnit.SECONDS);

// Execute periodically every 3 seconds (fixed rate)
ScheduledFuture<?> periodicTask = scheduler.scheduleAtFixedRate(() -> {
    System.out.println("Periodic task: " + System.currentTimeMillis());
}, 0, 3, TimeUnit.SECONDS);

// Execute with fixed delay between executions
scheduler.scheduleWithFixedDelay(() -> {
    System.out.println("Task with fixed delay between executions");
}, 0, 2, TimeUnit.SECONDS);

// Cancel periodic task after 15 seconds
scheduler.schedule(() -> {
    periodicTask.cancel(false);
    scheduler.shutdown();
}, 15, TimeUnit.SECONDS);

Difference between scheduling methods:

Back to top


Best Practices

1. Always Shutdown Executors

❌ Incorrect:

ExecutorService executor = Executors.newFixedThreadPool(4);
// Submit tasks
// Never shutdown - threads keep JVM alive!

✅ Correct:

ExecutorService executor = Executors.newFixedThreadPool(4);
try {
    // Submit tasks
    executor.submit(() -> doWork());
} finally {
    executor.shutdown();
    try {
        if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
            executor.shutdownNow();
        }
    } catch (InterruptedException e) {
        executor.shutdownNow();
        Thread.currentThread().interrupt();
    }
}

2. Size Thread Pools Appropriately

CPU-bound tasks:

int cores = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(cores);

I/O-bound tasks:

int cores = Runtime.getRuntime().availableProcessors();
// I/O tasks spend time waiting, so more threads can be beneficial
ExecutorService executor = Executors.newFixedThreadPool(cores * 2);

3. Handle Rejected Execution

When queue is full or executor is shutdown, tasks may be rejected:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(10),
    new ThreadPoolExecutor.CallerRunsPolicy() // Handle rejection
);

Rejection Policies:

4. Use Try-With-Resources (Java 19+)

try (ExecutorService executor = Executors.newFixedThreadPool(4)) {
    executor.submit(() -> doWork());
} // Automatically closes (shutdown)

Back to top


Common Pitfalls

❌ 1. Thread Pool Exhaustion

// BAD: Submitting blocking tasks that wait for each other
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<String> future1 = executor.submit(() -> {
    // This waits for future2 result
    return future2.get(); // DEADLOCK!
});
Future<String> future2 = executor.submit(() -> {
    return "result";
});

Solution: Use larger pool or separate executors for dependent tasks.

❌ 2. Not Handling InterruptedException

// BAD: Swallowing interruption
executor.submit(() -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        // Ignoring! Thread interrupt status lost
    }
});

✅ Correct:

executor.submit(() -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt(); // Preserve interrupt status
        // Clean up and exit
    }
});

❌ 3. Cached Thread Pool for Long-Running Tasks

// BAD: Can create thousands of threads
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 10000; i++) {
    executor.submit(() -> {
        // Long-running task (1 minute)
        Thread.sleep(60000);
    });
}
// May create 10,000 threads!

Solution: Use newFixedThreadPool for long-running tasks.

Back to top


Thread Pool Architecture

graph TB
    subgraph "Client Code"
        Client[Submit Tasks]
    end

    subgraph "ExecutorService"
        Queue[Task Queue<br/>BlockingQueue]
        Thread1[Worker Thread 1]
        Thread2[Worker Thread 2]
        Thread3[Worker Thread 3]
        ThreadN[Worker Thread N]
    end

    Client -->|submit| Queue
    Queue -.->|fetch task| Thread1
    Queue -.->|fetch task| Thread2
    Queue -.->|fetch task| Thread3
    Queue -.->|fetch task| ThreadN

    Thread1 -->|complete| Result1[Result/Future]
    Thread2 -->|complete| Result2[Result/Future]
    Thread3 -->|complete| Result3[Result/Future]
    ThreadN -->|complete| ResultN[Result/Future]

    style Queue fill:#ffe1e1
    style Thread1 fill:#e1f5ff
    style Thread2 fill:#e1f5ff
    style Thread3 fill:#e1f5ff
    style ThreadN fill:#e1f5ff

How it works:

  1. Client submits tasks to ExecutorService
  2. Tasks are placed in a BlockingQueue
  3. Worker threads pull tasks from queue
  4. Each worker executes its task
  5. Results are returned via Future objects
  6. Threads are reused for next tasks

Back to top


Ref.

Official Documentation:

Guides:


Get Started | Java Concurrency | Java 8