Java concurrency is both powerful and notoriously difficult to get right. From low-level synchronized blocks to high-level CompletableFuture pipelines and Java 21's virtual threads, the platform offers a layered concurrency model. Understanding the memory model, happens-before relationships, and when to use which abstraction separates senior Java engineers from juniors.

Key Points

  • Thread lifecycle: NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED
  • synchronized method/block: acquires the object's intrinsic lock — only one thread at a time, but no timeout or fairness
  • volatile: ensures visibility (prevents CPU caching) but NOT atomicity — volatile int i; i++ is still not thread-safe
  • ReentrantLock: explicit lock with tryLock(timeout), lockInterruptibly(), and optional fairness — always unlock in finally
  • AtomicInteger / AtomicReference: lock-free CAS (compare-and-swap) operations — faster than synchronized for single variables
  • ExecutorService: thread pool abstraction — Executors.newFixedThreadPool(n), newCachedThreadPool(), newScheduledThreadPool(n)
  • CompletableFuture (Java 8): async pipeline with thenApply, thenCompose, thenCombine, exceptionally, allOf, anyOf
  • Virtual Threads (Java 21): lightweight threads managed by the JVM, not the OS — create millions without exhausting memory; perfect for I/O-bound work
  • Java Memory Model (JMM): defines happens-before — a write to a volatile variable HB a subsequent read; monitor unlock HB subsequent lock
MechanismUse caseTimeout?Fairness?Overhead
synchronizedSimple mutual exclusionNoNoLow
ReentrantLockTimed/interruptible lockYesOptionalLow
ReadWriteLockMany readers, few writersYesOptionalLow
AtomicXxxSingle variable, lock-freeN/AN/AVery low
ExecutorServiceThread pool task submissionN/AN/AMedium
CompletableFutureAsync pipeline compositionN/AN/ALow
Virtual Thread (J21)Millions of I/O tasksN/AN/AVery low

Java concurrency: ReentrantLock, CompletableFuture pipeline, Java 21 Structured Concurrency and Virtual Threads

// ReentrantLock — always unlock in finally
private final ReentrantLock lock = new ReentrantLock();
public void transfer(Account to, int amount) {
    lock.lock();
    try { balance -= amount; to.balance += amount; }
    finally { lock.unlock(); }
}

// CompletableFuture pipeline
CompletableFuture<String> result = CompletableFuture
    .supplyAsync(() -> fetchUser(id))          // runs in ForkJoinPool
    .thenApply(user -> enrichWithOrders(user)) // transform
    .thenCompose(u -> CompletableFuture.supplyAsync(() -> notifyEmail(u)))
    .exceptionally(ex -> "default");           // fallback

// Combine two futures
CompletableFuture<UserData> data = CompletableFuture.allOf(futureA, futureB)
    .thenApply(v -> merge(futureA.join(), futureB.join()));

// Java 21 Virtual Threads — structured concurrency
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<User>   user    = scope.fork(() -> fetchUser(id));
    Future<Orders> orders  = scope.fork(() -> fetchOrders(id));
    scope.join().throwIfFailed();
    return new Page(user.resultNow(), orders.resultNow());
}

// Virtual thread executor — drop-in for thread-per-request
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 100_000).forEach(i ->
        exec.submit(() -> handleRequest(i))
    );
}

Real-World Example

Before virtual threads, a typical web server with a 200-thread pool could handle 200 concurrent requests. With virtual threads, the same server handles 100,000+ because I/O blocking unmounts the virtual thread from the carrier OS thread, freeing it for another task. Spring Boot 3.2+ and Tomcat 10 automatically use virtual threads when running on Java 21 — zero code changes needed.