Skip to content
  • ExecutorService & Thread Pools · Java

ExecutorService & Thread Pools

LearnViaNotes @learnvianotes· 5 min read

Don't create a thread per task — submit tasks to an ExecutorService, which reuses a pool of threads and gives you Futures for the results.

Why a pool?

submit, Future and invokeAll

Java
import java.util.List;
import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(3);
        try {
            Future<Integer> f = pool.submit(() -> 6 * 7);          // Callable<Integer>
            System.out.println("answer = " + f.get());            // blocks until done

            List<Callable<String>> tasks = List.of(() -> "a".repeat(3), () -> "b".repeat(2), () -> "c");
            for (Future<String> r : pool.invokeAll(tasks)) {       // results in task order
                System.out.println(r.get());
            }

            Future<?> g = pool.submit(() -> { });                  // Runnable
            System.out.println("runnable result = " + g.get());

            Future<Integer> bad = pool.submit(() -> 1 / 0);
            try {
                bad.get();
            } catch (ExecutionException e) {                        // the task's exception is wrapped
                System.out.println("cause: " + e.getCause());
            }
        } finally {
            pool.shutdown();                                        // stop accepting, finish queued
            System.out.println("terminated = " + pool.awaitTermination(1, TimeUnit.SECONDS));
        }
    }
}
Output
answer = 42
aaa
bb
c
runnable result = null
cause: java.lang.ArithmeticException: / by zero
terminated = true
Future methodPurpose
get() / get(t, unit)Wait for the result; throws ExecutionException (task failed), TimeoutException, InterruptedException
isDone()Completed normally, exceptionally or cancelled
cancel(mayInterrupt)Try to cancel; true interrupts a running task
resultNow() / state()Java 19+: read a completed result without checked exceptions

Executors factory methods

FactoryPoolWatch out
newFixedThreadPool(n)n threads, unbounded LinkedBlockingQueueQueue can grow until OutOfMemoryError
newCachedThreadPool()0…∞ threads, idle ones die after 60 s, SynchronousQueueUnbounded threads under load
newSingleThreadExecutor()1 thread, tasks run sequentially in orderSame unbounded queue
newScheduledThreadPool(n)Delayed / periodic tasks (schedule, scheduleAtFixedRate)Replaces java.util.Timer
newWorkStealingPool()ForkJoinPool with one queue per workerGood for many small CPU tasks
newVirtualThreadPerTaskExecutor()Java 21: one virtual thread per taskFor blocking I/O workloads

Shutting down

  1. shutdown()

    No new tasks accepted; already submitted ones still run.

  2. awaitTermination(t, unit)

    Block until all tasks finish or the timeout elapses; returns true if terminated.

  3. shutdownNow()

    Interrupt running tasks and return the ones never started (tasks must respond to interrupts).

Since Java 19 ExecutorService is AutoCloseable: close() calls shutdown() and waits for every task, so try-with-resources is the tidiest pattern.

Java
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Main {
    public static void main(String[] args) throws Exception {
        List<Future<Integer>> squares = new ArrayList<>();
        try (ExecutorService pool = Executors.newFixedThreadPool(4)) {
            for (int i = 1; i <= 5; i++) {
                int n = i;
                squares.add(pool.submit(() -> n * n));
            }
        }                                   // close(): shutdown + wait for all tasks
        int total = 0;
        for (Future<Integer> f : squares) total += f.resultNow();
        System.out.println("sum of squares = " + total);
    }
}
Output
sum of squares = 55

ThreadPoolExecutor: the real knobs

ParameterMeaning
corePoolSizeThreads kept alive even when idle
maximumPoolSizeUpper bound — extra threads are created only when the queue is full
keepAliveTimeHow long threads above core may stay idle
workQueueWhere waiting tasks go: bounded ArrayBlockingQueue recommended
threadFactoryName threads (order-worker-1), set daemon/priority
handlerRejection policy when both queue and max threads are full

task arrives → threads < core? new thread → else queue it → queue full? thread up to max → else reject

Java
import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch gate = new CountDownLatch(1);
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
            1, 1, 0, TimeUnit.SECONDS,                 // exactly one thread
            new ArrayBlockingQueue<>(1),               // room for one waiting task
            new ThreadPoolExecutor.AbortPolicy());     // default: throw when full
        Runnable blocker = () -> {
            try { gate.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        };
        pool.execute(blocker);                         // runs on the only thread
        pool.execute(blocker);                         // waits in the queue
        try {
            pool.execute(blocker);                     // no thread, no queue space
        } catch (RejectedExecutionException e) {
            System.out.println("3rd task rejected");
        }
        System.out.println("pool size = " + pool.getPoolSize() + ", queued = " + pool.getQueue().size());
        gate.countDown();
        pool.shutdown();
        pool.awaitTermination(1, TimeUnit.SECONDS);
        System.out.println("completed = " + pool.getCompletedTaskCount());
    }
}
Output
3rd task rejected
pool size = 1, queued = 1
completed = 2
Rejection policyBehaviour
AbortPolicy (default)Throw RejectedExecutionException
CallerRunsPolicyRun the task on the submitting thread — natural back-pressure
DiscardPolicySilently drop the new task
DiscardOldestPolicyDrop the oldest queued task, retry the new one

How many threads?

threads ≈ cores × (1 + wait time / compute time)

  • CPU-bound (hashing, parsing, math): about Runtime.getRuntime().availableProcessors() threads — more only adds context switching.

  • I/O-bound (HTTP calls, DB queries): many more, since threads mostly wait. On Java 21 prefer virtual threads here.

  • Use separate pools for separate kinds of work, so a slow dependency can't starve everything (bulkheads).

submit() vs execute()?

Why is Executors.newFixedThreadPool considered risky in production?

When does a ThreadPoolExecutor create threads beyond corePoolSize?

shutdown() vs shutdownNow()?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report