⚡IntermediateConcurrency Basics

ExecutorService

Manage thread pools and execute tasks asynchronously with ExecutorService and Future.

Overview

Managing individual Thread objects is tedious and error-prone. Thread creation is expensive: spinning up a new thread for every small task wastes resources. ExecutorService manages a pool of reusable threads. You submit tasks (Runnables or Callables); the pool handles scheduling, thread creation, and reuse. When work is done, you shut down the pool. Callable<T> is like Runnable but can return a result and throw checked exceptions. submit() returns a Future<T> representing the pending result. future.get() blocks until the result is available.

Key Points

  • •Executors.newFixedThreadPool(n): pool of exactly n threads
  • •Executors.newSingleThreadExecutor(): one thread, tasks run sequentially
  • •Executors.newCachedThreadPool(): creates threads as needed, reuses idle ones
  • •executor.submit(runnable) or submit(callable) returns a Future
  • •Future.get() blocks until the result is ready
  • •Future.isDone() checks without blocking
  • •executor.shutdown() stops accepting new tasks; awaits completion
  • •executor.shutdownNow() attempts to cancel running tasks immediately
  • •awaitTermination(timeout, unit) waits for the pool to stop

Examples

Fixed thread pool with Runnable

import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        for (int i = 1; i <= 6; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.printf("Task %d started on %s%n", taskId, Thread.currentThread().getName());
                try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
                System.out.printf("Task %d done%n", taskId);
            });
        }

        executor.shutdown(); // no more tasks accepted
        executor.awaitTermination(5, TimeUnit.SECONDS); // wait for all tasks
        System.out.println("All tasks complete");
    }
}

With 3 threads and 6 tasks, the pool runs 3 tasks concurrently. When a thread finishes a task, it picks the next one from the queue. This is much more efficient than creating 6 threads.

Callable and Future: getting results

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

public class Main {
    static long fibonacci(int n) {
        if (n <= 1) return n;
        long a = 0, b = 1;
        for (int i = 2; i <= n; i++) { long c = a + b; a = b; b = c; }
        return b;
    }

    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(4);

        int[] inputs = {40, 42, 44, 46};
        List<Future<Long>> futures = new ArrayList<>();

        for (int n : inputs) {
            futures.add(executor.submit(() -> fibonacci(n)));
        }

        // Collect results (get() blocks until each future is done)
        for (int i = 0; i < inputs.length; i++) {
            System.out.printf("fib(%d) = %d%n", inputs[i], futures.get(i).get());
        }

        executor.shutdown();
    }
}

submit(Callable) returns Future<Long>. Calling get() waits for that specific task to finish. The tasks may complete in any order, but we collect results in submission order.

invokeAll: wait for a batch of tasks

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

public class Main {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        List<Callable<String>> tasks = new ArrayList<>();
        for (int i = 1; i <= 5; i++) {
            final int id = i;
            tasks.add(() -> {
                Thread.sleep(id * 50L);
                return "Result from task " + id;
            });
        }

        // invokeAll blocks until all tasks are done, then returns their Futures
        List<Future<String>> results = executor.invokeAll(tasks, 10, TimeUnit.SECONDS);
        for (Future<String> f : results) {
            System.out.println(f.get()); // each future is already done
        }

        executor.shutdown();
    }
}

invokeAll() submits all tasks and blocks until all complete (or the timeout expires). Unlike calling get() on each Future individually, invokeAll guarantees all are done before you iterate.

Common Mistakes

  • Not calling executor.shutdown(). The JVM will not exit because the thread pool threads keep the JVM alive even after main() returns
  • Calling future.get() immediately in a loop before all tasks are submitted. This defeats the purpose: you wait for each task to finish before submitting the next, achieving no concurrency
  • Ignoring ExecutionException from future.get(). When a Callable throws, the exception is wrapped in ExecutionException. Always catch it and call getCause() to see the original exception

Note

For CPU-bound tasks, set the thread pool size to the number of available processors: Runtime.getRuntime().availableProcessors(). For I/O-bound tasks, use a larger pool because threads spend most time waiting.

Exercises

Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.

Loading editor…

Completed: 0 / 4 exercises

Knowledge Check

Question 1 of 4

What is the difference between Runnable and Callable?