Audience: Beginner to Architect
JDK Context: Java 8 → Java 21 (including Virtual Threads and Structured Concurrency)
Scope: Threads, memory model, synchronization, executors, Fork/Join, CompletableFuture, virtual threads, and architectural concurrency patterns.
Concurrency is one of the defining strengths of the Java platform. From early Thread APIs to modern constructs like CompletableFuture, ForkJoinPool, and Virtual Threads (Project Loom), Java has evolved into a powerful platform for building scalable, concurrent systems.
Understanding concurrency is not just about writing multi-threaded code. It is about:
This article walks from foundational concepts to modern concurrency architecture in Java 21.
A thread is a lightweight unit of execution within a process.
Each Java application starts with at least one thread:
Additional threads allow multiple tasks to execute concurrently.
Historically, Java threads are mapped 1:1 to OS threads.
Java Thread 1 → OS Thread 1
Java Thread 2 → OS Thread 2
Java Thread 3 → OS Thread 3
This model is called platform threads.
Platform threads are:
A thread in Java goes through several states.
stateDiagram-v2 [*] --> New New --> Runnable Runnable --> Running Running --> Blocked Blocked --> Runnable Running --> Terminated
| State | Meaning |
|---|---|
| NEW | Thread object created but not started |
| RUNNABLE | Eligible to run |
| BLOCKED | Waiting for monitor lock |
| WAITING | Waiting indefinitely |
| TIMED_WAITING | Waiting with timeout |
| TERMINATED | Finished execution |
Concurrency correctness depends on memory visibility.
Modern CPUs reorder instructions for optimization. Without memory rules, threads may see stale values.
The Java Memory Model defines:
If A happens-before B, then B sees effects of A.
Examples:
A race condition occurs when two threads access shared mutable state without synchronization.
Example:
class Counter {
int count = 0;
void increment() {
count++;
}
}
count++ is not atomic. It expands to:
Two threads may interleave operations and lose updates.
Java provides multiple synchronization mechanisms.
synchronized (lock) {
// critical section
}
Each object has a monitor.
Thread A → acquires monitor → executes → releases monitor
Thread B → waits
More flexible than synchronized.
Features:
volatile ensures:
It does NOT guarantee atomicity.
Thread A writes volatile variable
Thread B immediately sees updated value
Use volatile for:
Package: java.util.concurrent.atomic
Example:
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();
Internally uses:
Creating threads manually is inefficient.
Java provides the Executor framework.
Task → Executor → Thread Pool → Worker Threads
ExecutorService executor = Executors.newFixedThreadPool(4);
Best for:
Executors.newCachedThreadPool();
Best for:
Designed for parallelizable divide-and-conquer tasks.
Used internally by:
Each worker thread has its own deque.
Worker 1: [Task A, Task B]
Worker 2: [Task C]
Worker 3: []
Worker 3 steals from Worker 1
This improves load balancing.
graph TD A[Task] --> B[Subtask 1] A --> C[Subtask 2] B --> D[Combine] C --> D
Modern asynchronous programming model.
Supports:
Example:
CompletableFuture
.supplyAsync(() -> fetch())
.thenApply(data -> transform(data))
.thenAccept(result -> save(result));
Enables structured asynchronous flow without explicit thread management.
Introduced as stable in Java 21.
Virtual threads are lightweight threads managed by the JVM.
Platform Thread → OS Thread
Virtual Thread → JVM Managed → Mounted on Platform Thread
graph TD A[Virtual Thread 1] --> P1[Platform Thread] B[Virtual Thread 2] --> P1 C[Virtual Thread 3] --> P2
Thousands of virtual threads can run on a few platform threads.
Traditional blocking I/O consumes platform threads.
With virtual threads:
Example:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> handleRequest());
}
Structured Concurrency provides:
Conceptually:
Parent Task
├── Child Task A
├── Child Task B
└── Child Task C
All children must complete before parent finishes.
| Model | Best For |
|---|---|
| Thread-per-request | Virtual threads |
| Event loop | Reactive systems |
| Fork/Join | CPU-bound tasks |
| CompletableFuture | Async pipelines |
| Actor model | Distributed systems |
graph LR A[Thread 1] -->|holds Lock A| B[Lock A] B -->|waits for| C[Lock B] D[Thread 2] -->|holds Lock B| C C -->|waits for| B
Avoid by:
Key metrics:
For CPU-bound tasks:
For I/O-bound workloads:
Modern Java concurrency has evolved from low-level thread manipulation to highly expressive, scalable concurrency models.
With Java 21:
Understanding concurrency today means understanding both:
Concurrency is not simply about running tasks in parallel. It is about designing systems that behave predictably under load while maintaining clarity, maintainability, and correctness.