Matheion

MA-JAVA0 · Module 25

Executors, futures, and concurrent utilities

Topics: ExecutorService · Future · AtomicInteger · ConcurrentHashMap

Overview

Session 25 — Executors, futures, and concurrent utilities

Hand-rolling new Thread for every job does not scale. This session moves to ExecutorService: a pool of workers, Future for a result that arrives later, AtomicInteger for a single counter, and ConcurrentHashMap for a shared lookup table. A short note on virtual threads (newer JDKs) closes the path so you know what names to look up next.

Time: about 95 minutes.

Concepts

1. Why not new Thread every time?

Each start() creates a worker, allocates a stack, and later tears it down. A ticket booth that handles hundreds of short “print this pass” jobs should reuse workers. An ExecutorService is a queue of tasks plus a set of threads that drain the queue.

You submit Runnable (no result) or Callable<V> (returns V, may throw a checked exception). The pool decides which worker runs which job.


2. Building and shutting down a pool

ExecutorService pool = Executors.newFixedThreadPool(3);
try {
    pool.submit(() -> System.out.println("pass A"));
    pool.submit(() -> System.out.println("pass B"));
} finally {
    pool.shutdown();
}

newFixedThreadPool(n) keeps up to n threads. newSingleThreadExecutor() is one worker—useful when jobs must not overlap. newCachedThreadPool() grows aggressively; skip it until you know you need it.

shutdown() stops accepting new tasks; running and queued tasks still finish. shutdownNow() tries to interrupt workers and drops waiting tasks. awaitTermination waits (with a timeout) after shutdown.

Forgetting shutdown is a classic “my main hung” bug: non-daemon pool threads keep the JVM alive.

Java 19+ ExecutorService is AutoCloseable; try (var pool = Executors.newFixedThreadPool(3)) { ... } calls shutdown for you. The try/finally form works on older JDKs used in many classrooms.


3. Future: a value that is not here yet

Future<Integer> seats = pool.submit(() -> countManifest());
int n = seats.get(); // blocks until the callable returns

get() waits. get(timeout, unit) waits at most that long and throws TimeoutException. cancel(true) requests interrupt of the worker if the task is running.

If the callable throws, get() wraps it in ExecutionException. Call getCause() to see the original failure.

Future is not a full reactive pipeline. It is “I submitted work; I will pick up the result.” For many intro programs that is enough.


4. AtomicInteger for a single counter

AtomicInteger offers incrementAndGet, addAndGet, compareAndSet—updates that are atomic without you writing synchronized.

Good fit: a lone hit counter or remaining seats if that integer is the entire invariant.

Poor fit: “decrement seats and record lastBuyer.” Those two writes need one lock or one concurrent structure that updates together. Mixing AtomicInteger for seats with an unsynchronized String lastBuyer reintroduces a race between fields.


5. ConcurrentHashMap for a shared directory

HashMap is not safe for concurrent mutation. Two threads putting keys can corrupt the table. ConcurrentHashMap allows concurrent reads and updates. Per-key operations such as putIfAbsent and compute are designed for this.

It is still possible to write check-then-act bugs around the map:

if (!map.containsKey(id)) { // gap: another thread inserts here
    map.put(id, hold);
}

Use putIfAbsent or computeIfAbsent so the check and insert are one atomic story for that key.


6. Virtual threads (map of the territory)

On Java 21+, Thread.startVirtualThread(runnable) and Executors.newVirtualThreadPerTaskExecutor() create virtual threads: many lightweight tasks scheduled onto a smaller set of platform threads.

They shine for lots of waiting (I/O). They do not remove data races. synchronized still works; pinning and pool sizing are advanced topics. If your JDK is older, skip the APIs and remember the name so later reading makes sense.


7. Common confusions

“The pool is 3, so at most 3 tasks exist.” At most 3 run at once. You may submit hundreds; they queue.

“submit runs on main.” submit enqueues. main continues. get() is where main may block.

“ConcurrentHashMap means my whole class is thread-safe.” Only the map operations. Invariants that span the map and another field still need a design.


8. What you should be able to do

Submit several jobs to a fixed pool, shutdown cleanly, retrieve a Future result, and pick AtomicInteger vs synchronized vs ConcurrentHashMap by what invariant you are protecting. That is the end of this concurrency sequence in the foundations path.

Examples

Example A — fixed pool and shutdown

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class PassPool {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(3);
        try {
            for (int i = 1; i <= 6; i++) {
                int n = i;
                pool.submit(() -> System.out.println("pass " + n
                    + " on " + Thread.currentThread().getName()));
            }
        } finally {
            pool.shutdown();
            pool.awaitTermination(2, TimeUnit.SECONDS);
        }
    }
}

Six jobs, three workers. Thread names will repeat (pool-1-thread-1, …). n is copied per iteration so the lambda does not capture a mutating loop index (Session 21’s effectively-final rule).


Example B — Callable and Future.get

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ManifestTotal {
    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newSingleThreadExecutor();
        try {
            Future<Integer> total = pool.submit(() -> {
                int sum = 0;
                for (int n : new int[] {12, 7, 9}) {
                    sum += n;
                }
                return sum;
            });
            System.out.println("seats on manifest: " + total.get());
        } catch (ExecutionException e) {
            throw e;
        } finally {
            pool.shutdown();
        }
    }
}

The callable runs on the pool thread. get() brings 28 back to main. If the lambda threw, ExecutionException would wrap it.


Example C — AtomicInteger vs extra fields

import java.util.concurrent.atomic.AtomicInteger;

public class SeatRoll {
    private final AtomicInteger remaining = new AtomicInteger(40);

    public boolean sellOne() {
        while (true) {
            int n = remaining.get();
            if (n <= 0) {
                return false;
            }
            if (remaining.compareAndSet(n, n - 1)) {
                return true;
            }
        }
    }
}

compareAndSet retries if another clerk won the decrement. Fine while the only shared state is this integer. The moment you also set lastBuyer without a lock, you are back in Session 23.


Example D — computeIfAbsent instead of contains-then-put

import java.util.concurrent.ConcurrentHashMap;

public class Holds {
    private final ConcurrentHashMap<String, String> byPassenger =
        new ConcurrentHashMap<>();

    public String holdBerth(String passenger, String berth) {
        return byPassenger.computeIfAbsent(passenger, p -> berth);
    }
}

First caller for "Rafi" wins the berth string. A later call returns the existing hold instead of overwriting. That is the concurrent-map habit; a separate containsKey + put pair is the gap.

Practice check — executors and concurrent utilities

Multiple-choice check from the Matheion app lesson. Pick an answer for each question, then submit.

  1. 1.`Executors.newFixedThreadPool(3)` with 10 `submit` calls means…

  2. 2.Forgetting `pool.shutdown()` after submitting work often causes…

  3. 3.`Future<Integer> f = pool.submit(callable);` then `f.get()`…

  4. 4.`AtomicInteger` is a good fit when…

  5. 5.Safer than `if (!map.containsKey(k)) map.put(k, v)` on a `ConcurrentHashMap` is…