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
-
thenApplytransforms the value;thenAcceptconsumes it;thenRunignores it. -
thenComposeflattens a stage that itself returns aCompletableFuture(theflatMapof futures). -
thenCombinemerges two unrelated stages;allOf/anyOffan several in. -
exceptionallysupplies a fallback value;handlereceives both the result and the exception (either may benull) 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:
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
synchronizedcan pin a carrier whereReentrantLockdoes not. -
Streams and Collectors —
parallelStream()for data parallelism, a different tool from these task-oriented APIs. -
Functional Programming — the
Function/Suppliertypes eachCompletableFuturestage consumes.