Home/Learn/Java A–Z/ExecutorService and Thread Pools

ExecutorService and Thread Pools

Intermediate
Concurrency

ExecutorService manages a pool of reusable threads, decoupling task submission from execution and avoiding thread-creation overhead.

Overview

Creating a new Thread for every task is expensive and uncontrolled. ExecutorService (java.util.concurrent) manages a pool of worker threads that pick up submitted tasks. Executors provides factory methods for common pool types. ThreadPoolExecutor exposes full configuration. ScheduledExecutorService handles recurring and delayed tasks. Always shut down executor services to release threads.

Creating and Using ExecutorService

Executors.newFixedThreadPool(n) creates a pool of n threads. Tasks are queued when all threads are busy. submit() returns a Future; execute() fires and forgets.

Always shut down the executor to release threads — use shutdown() for graceful termination or shutdownNow() for immediate stop.

ExecutorBasics.java
import java.util.concurrent.*;

// Fixed pool — n threads, unbounded queue
ExecutorService pool = Executors.newFixedThreadPool(4);

// Submit a Runnable (no return value)
pool.execute(() -> System.out.println("Task 1"));

// Submit a Callable (returns a value)
Future<Integer> future = pool.submit(() -> {
    Thread.sleep(100);
    return 42;
});

// Block until result is ready
int result = future.get(); // 42
int resultWithTimeout = future.get(5, TimeUnit.SECONDS);

// Graceful shutdown
pool.shutdown();                           // no new tasks accepted
pool.awaitTermination(10, TimeUnit.SECONDS); // wait for in-flight tasks
// Force shutdown if still running
if (!pool.isTerminated()) pool.shutdownNow();

Common Pool Types

Fixed pool: bounded threads, good for CPU-bound tasks. Cached pool: creates threads on demand, reuses idle ones — good for short-lived I/O tasks. Single-thread executor: sequential execution, tasks never run concurrently. Scheduled executor: run after delay or at fixed rate.

For Java 8+, ForkJoinPool.commonPool() is used by parallel streams and CompletableFuture.

PoolTypes.java
// Fixed pool — CPU-bound (size = # of cores)
ExecutorService fixed = Executors.newFixedThreadPool(
    Runtime.getRuntime().availableProcessors());

// Cached pool — I/O-bound (threads grow/shrink dynamically)
ExecutorService cached = Executors.newCachedThreadPool();

// Single thread — guaranteed sequential execution
ExecutorService single = Executors.newSingleThreadExecutor();

// Scheduled — delayed or periodic tasks
ScheduledExecutorService scheduler =
    Executors.newScheduledThreadPool(2);

// Run once after 5 seconds
scheduler.schedule(() -> System.out.println("Delayed"), 5, TimeUnit.SECONDS);

// Run every 10 seconds (fixed rate — counts from start)
scheduler.scheduleAtFixedRate(
    () -> System.out.println("Heartbeat"),
    0, 10, TimeUnit.SECONDS);

// Run 10 seconds after previous completion (fixed delay)
scheduler.scheduleWithFixedDelay(
    () -> pollDatabase(),
    0, 10, TimeUnit.SECONDS);

ThreadPoolExecutor Configuration

ThreadPoolExecutor exposes full control: core pool size (minimum threads kept alive), max pool size, keep-alive time (how long idle threads above core survive), work queue, rejection policy.

Rejection policies: AbortPolicy (throws), CallerRunsPolicy (caller thread runs the task), DiscardPolicy (silently drops), DiscardOldestPolicy (drops oldest queued task).

ThreadPoolConfig.java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4,                              // corePoolSize
    8,                              // maximumPoolSize
    60L, TimeUnit.SECONDS,          // keepAliveTime (idle threads above core)
    new LinkedBlockingQueue<>(100), // work queue (bounded)
    new ThreadFactory() {
        private final AtomicInteger n = new AtomicInteger(0);
        @Override public Thread newThread(Runnable r) {
            Thread t = new Thread(r, "worker-" + n.getAndIncrement());
            t.setDaemon(true);
            return t;
        }
    },
    new ThreadPoolExecutor.CallerRunsPolicy() // back-pressure
);

// Monitor the pool
System.out.println("Pool size:   " + executor.getPoolSize());
System.out.println("Active:      " + executor.getActiveCount());
System.out.println("Queue size:  " + executor.getQueue().size());
System.out.println("Completed:   " + executor.getCompletedTaskCount());

Interactive Visualization

NEWRUNNABLERUNNINGBLOCKEDWAITINGTERMINATED
synchronized(lock)— free
main
RUNNING
t1
NEW
t2
NEW
main thread creates Thread t1 and Thread t2. Both are in NEW state.
1 / 6

Key Points to Remember

  • Never create raw threads in production — use an ExecutorService.
  • submit() returns Future; execute() is fire-and-forget.
  • Always call shutdown() and awaitTermination() to gracefully stop the pool.
  • Fixed pool for CPU-bound; cached pool for I/O-bound; scheduled pool for timed tasks.
  • ThreadPoolExecutor provides full control over core size, max size, queue, and rejection policy.

Practice ExecutorService and Thread Pools in the Playground

Run and modify code directly in your browser - no setup needed.

Interview Questions

Sign in to ask Aria
1

What is the difference between execute() and submit() in ExecutorService?

EasyAmazon
2

What happens if you submit a task to a shut-down ExecutorService?

EasyGoogle
3

What is the difference between shutdown() and shutdownNow()?

MediumOracle
4

How does CallerRunsPolicy provide back-pressure?

MediumNetflix
5

How do you determine the right thread pool size for CPU-bound vs I/O-bound tasks?

HardMicrosoft

Ask Aria about ExecutorService and Thread Pools

Your personal AI tutor — ask anything about this concept