High-Level Concurrency

This section documents the current Java release line, with Java 25 LTS as the reference point (no specific patch version is pinned), as published at the Java developer portal, the Java Tutorials, and the Java SE API specification — which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Raw Thread objects and synchronized blocks (see Concurrency Basics) are the foundation, but application code rarely uses them directly. The java.util.concurrent package supplies task-oriented abstractions instead — thread pools, futures, thread-safe collections, and higher-level locks. This page covers executors, CompletableFuture, concurrent data structures, and the synchronizers, following the Java Tutorials High Level Concurrency Objects lesson.

Executors and Thread Pools

An Executor decouples submitting a task from how it runs. ExecutorService adds lifecycle control and returns a Future for each submitted job. The Executors factory builds the common pool shapes: newFixedThreadPool, newCachedThreadPool, newSingleThreadExecutor, and newVirtualThreadPerTaskExecutor. See Executors and Thread Pools.

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

try (ExecutorService pool = Executors.newFixedThreadPool(4)) {   // AutoCloseable since Java 19

    Future<Integer> f = pool.submit(() -> 21 * 2);
    System.out.println(f.get());                                 // 42 -- blocks until ready

    List<Callable<String>> tasks = List.of(() -> "a", () -> "b", () -> "c");
    for (Future<String> r : pool.invokeAll(tasks)) {             // waits for all
        System.out.println(r.get());
    }
}   // close() runs shutdown() then awaits termination of every submitted task

Without try-with-resources, drive the lifecycle by hand: shutdown() stops accepting work and lets queued tasks finish, awaitTermination blocks for a bounded time, and shutdownNow() interrupts whatever is left.

ExecutorService pool = Executors.newCachedThreadPool();
pool.submit(job);
pool.shutdown();
if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
    pool.shutdownNow();
}

Composing Async Work with CompletableFuture

CompletableFuture is a Future you can chain: each stage names the callback to run when the previous one completes, so independent work overlaps without blocking a thread on get(). Start a stage with supplyAsync (returns a value) or runAsync (side effect only), optionally on a supplied Executor.

import java.util.concurrent.*;

Executor io = Executors.newVirtualThreadPerTaskExecutor();

CompletableFuture<Integer> price = CompletableFuture.supplyAsync(() -> fetchPrice("AAPL"), io);
CompletableFuture<Double>  rate  = CompletableFuture.supplyAsync(() -> fetchRate("USD", "EUR"), io);

CompletableFuture<String> quote = price
        .thenCombine(rate, (p, r) -> p * r)          // combine two independent results
        .thenApply(total -> "EUR " + total)          // map the value (like Stream.map)
        .exceptionally(ex -> "unavailable: " + ex.getMessage());   // recover from any failure

System.out.println(quote.join());                    // join(): like get() but unchecked
  • thenApply transforms the value; thenAccept consumes it; thenRun ignores it.

  • thenCompose flattens a stage that itself returns a CompletableFuture (the flatMap of futures).

  • thenCombine merges two unrelated stages; allOf / anyOf fan several in.

  • exceptionally supplies a fallback value; handle receives both the result and the exception (either may be null) and always produces a value.

// thenCompose: id lookup, then a dependent load
CompletableFuture<User> user = CompletableFuture.supplyAsync(() -> userId(request), io)
        .thenCompose(id -> CompletableFuture.supplyAsync(() -> loadUser(id), io));

// allOf: proceed once every future finishes
CompletableFuture.allOf(price, rate).thenRun(() -> System.out.println("both done"));

// handle: collapse success or failure into one value
CompletableFuture<Integer> safe = price.handle((value, ex) -> ex != null ? -1 : value);

The pipeline above forms this shape — two async producers feeding a combine, then a recovery stage:

flowchart LR A["supplyAsync: fetchPrice"] --> C["thenCombine: p * r"] B["supplyAsync: fetchRate"] --> C C --> D["thenApply: format as EUR string"] D --> E{"completed exceptionally?"} E -->|no| F["join returns the value"] E -->|yes| G["exceptionally: supply fallback"] G --> F

Concurrent Collections and Atomics

The synchronized wrappers from Collections lock the whole structure per call. The concurrent collections scale better: ConcurrentHashMap allows concurrent reads and striped writes with atomic compound operations, CopyOnWriteArrayList copies its backing array on every write (ideal for rarely-changed listener lists), and a BlockingQueue such as ArrayBlockingQueue hands work between producer and consumer threads.

import java.util.concurrent.*;

ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
counts.merge("hits", 1, Integer::sum);              // atomic read-modify-write
counts.computeIfAbsent("users", k -> 0);

CopyOnWriteArrayList<Runnable> listeners = new CopyOnWriteArrayList<>();

BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
queue.put("job-1");                                 // blocks if full
String job = queue.take();                          // blocks if empty

The java.util.concurrent.atomic package offers lock-free single variables — see Atomic Variables. AtomicInteger and AtomicReference give compareAndSet; LongAdder outperforms AtomicLong as a pure counter under heavy contention.

import java.util.concurrent.atomic.*;

AtomicInteger seq = new AtomicInteger();
int next = seq.incrementAndGet();

AtomicReference<String> leader = new AtomicReference<>("none");
leader.compareAndSet("none", "node-a");             // set only if unchanged

LongAdder requests = new LongAdder();
requests.increment();
long total = requests.sum();

Locks, Synchronizers, and Reactive Streams

The java.util.concurrent.locks package generalises synchronized. Prefer synchronized for simple mutual exclusion; reach for ReentrantLock when you need tryLock, a timeout, interruptible acquisition, fairness, or several condition queues. A ReadWriteLock lets many readers share access; StampedLock adds an optimistic read that acquires nothing. See Lock Objects.

import java.util.concurrent.locks.*;

ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // critical section
} finally {
    lock.unlock();                                  // release in finally, always
}

StampedLock sl = new StampedLock();
long stamp = sl.tryOptimisticRead();
double x = sharedX, y = sharedY;
if (!sl.validate(stamp)) {                          // a writer intervened -- retry under a real lock
    stamp = sl.readLock();
    try { x = sharedX; y = sharedY; } finally { sl.unlockRead(stamp); }
}

The synchronizers coordinate groups of threads: CountDownLatch waits for N one-off events, Semaphore caps concurrent access to a resource, CyclicBarrier releases a fixed set of threads together and resets, and Phaser is a reusable barrier whose party count can change at run time.

import java.util.concurrent.*;

CountDownLatch ready = new CountDownLatch(3);
// each worker on completion: ready.countDown();
ready.await();                                      // returns once the count hits zero

Semaphore permits = new Semaphore(5);
permits.acquire();
try {
    // at most 5 threads here at once
} finally {
    permits.release();
}

CyclicBarrier barrier = new CyclicBarrier(4, () -> System.out.println("phase complete"));
// each of the 4 workers: barrier.await();

The java.util.concurrent.Flow class holds the four nested interfaces — Flow.Publisher, Flow.Subscriber, Flow.Subscription, Flow.Processor — that define the reactive-streams contract: asynchronous, non-blocking data flow with back-pressure, where a subscriber requests only as many items as it can handle. SubmissionPublisher is a ready-made publisher. The JDK ships only these interfaces; full reactive frameworks (Reactor, RxJava) build on the same contract and are out of scope here.

See Also

  • Concurrency Basics — threads, Runnable, synchronized, and the memory model these APIs are layered on.

  • Virtual Threads — the executor to hand blocking tasks, and why synchronized can pin a carrier where ReentrantLock does not.

  • Streams and Collectors — parallelStream() for data parallelism, a different tool from these task-oriented APIs.

  • Functional Programming — the Function / Supplier types each CompletableFuture stage consumes.