Threads & Concurrency
Java threads are the foundation, but real concurrency code uses java.util.concurrent — ExecutorService for thread pools, CompletableFuture for async pipelines, locks/semaphores for coordination, and virtual threads (Java 21+) for cheap blocking I/O.
ExecutorService, CompletableFuture, virtual threads
EXAMPLE
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import java.time.Duration;
import java.util.List;
public class ThreadsDemo {
// 1) Fixed thread pool — bounded, predictable
static final ExecutorService POOL = Executors.newFixedThreadPool(8);
static long blocking(long ms) throws InterruptedException {
Thread.sleep(Duration.ofMillis(ms));
return ms;
}
public static void main(String[] args) throws Exception {
// 2) Submit + Future
Future<Long> f = POOL.submit(() -> blocking(50));
System.out.println("got " + f.get(1, TimeUnit.SECONDS));
// 3) invokeAll — wait for many
var tasks = List.<Callable<Long>>of(
() -> blocking(30), () -> blocking(50), () -> blocking(20));
List<Future<Long>> done = POOL.invokeAll(tasks);
for (var fut : done) System.out.println(fut.get());
// 4) CompletableFuture — async pipeline
var stage = CompletableFuture
.supplyAsync(() -> fetchData(), POOL)
.thenApply(s -> s.toUpperCase())
.thenCompose(s -> CompletableFuture.supplyAsync(() -> s.length(), POOL))
.exceptionally(ex -> -1);
System.out.println("length = " + stage.get());
// 5) Combine many — allOf for fan-out / fan-in
var f1 = CompletableFuture.supplyAsync(() -> "a", POOL);
var f2 = CompletableFuture.supplyAsync(() -> "b", POOL);
var f3 = CompletableFuture.supplyAsync(() -> "c", POOL);
CompletableFuture.allOf(f1, f2, f3).join();
System.out.println(f1.get() + f2.get() + f3.get());
// 6) Atomic counters — lockless
AtomicLong counter = new AtomicLong(0);
for (int i = 0; i < 1000; i++) POOL.submit(() -> counter.incrementAndGet());
// 7) Locks for multi-field state
Object lock = new Object();
synchronized (lock) {
// critical section
}
// For more control: ReentrantLock, ReadWriteLock, StampedLock
// 8) Virtual threads (Java 21+) — millions of cheap threads for blocking IO
try (var vexec = Executors.newVirtualThreadPerTaskExecutor()) {
var v = vexec.submit(() -> blocking(10));
System.out.println("virtual got " + v.get());
}
// 9) Structured concurrency (preview in 21, stable in 22+)
// try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// var a = scope.fork(() -> fetchUser(id));
// var b = scope.fork(() -> fetchOrders(id));
// scope.join().throwIfFailed();
// render(a.get(), b.get());
// }
// 10) Shutdown — never forget
POOL.shutdown();
if (!POOL.awaitTermination(10, TimeUnit.SECONDS)) {
POOL.shutdownNow();
}
}
static String fetchData() { return "hello"; }
// ===== Pitfalls =====
// - Forgetting executor.shutdown() -> threads keep the JVM alive
// - Calling .get() without a timeout -> hangs forever on failure
// - Sharing mutable state without sync -> data races
// - Using newCachedThreadPool() unbounded -> can spawn thousands under load
// - Doing CPU work on the common ForkJoinPool -> blocks parallel streams elsewhere
// - Mixing virtual threads with synchronized blocks holding I/O -> pinning
}
Why it matters
For blocking I/O on Java 21+, use a virtual-thread executor (`Executors.newVirtualThreadPerTaskExecutor()`) and stop fighting thread pool sizes. Reserve fixed pools for CPU-bound work where the optimal pool size is "available cores"; let virtual threads handle the "thousands of waiting on a remote call" case the JVM was never great at before.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
new Thread(() -> System.out.println("in thread")).start();
var pool = Executors.newFixedThreadPool(4);
pool.submit(() -> work());
Try it Yourself »
Discussion
Loading…