Language and Runtime Concurrency Models: Java

Java was the first mainstream language with threads and a memory model built in.

Learning Objectives

Use Thread, Runnable, Callable; synchronized; Lock, ReentrantLock and conditions; atomic classes; volatile under the JMM.

Use ExecutorService, Future and CompletableFuture; fork/join; concurrent collections; virtual threads (Java 21); know structured concurrency's preview status; use NIO and asynchronous I/O.

Diagnose with thread dumps and Java Flight Recorder.

Plain-Language Explanation

Java was the first mainstream language with threads and a memory model built in. Its toolkit grew in layers: synchronized and wait/notify (1995), java.util.concurrent with executors, locks, atomics and concurrent collections (Java 5), fork/join (7), CompletableFuture (8), and virtual threads (21) — which bring back simple thread-per-task code at massive scale.

Precise Technical Explanation

Thread, Runnable and Callable Runnable returns nothing and cannot throw checked exceptions; Callable<V> returns a value and may throw. Thread.start() runs on a new thread (run() would not). Builders: Thread.ofPlatform(), Thread.ofVirtual() (21).

Every object has an intrinsic monitor: synchronized methods/blocks lock it re-entrantly; exiting the block (normally or by exception) unlocks and publishes writes; wait()/notify()/notifyAll() must be called while holding it.

Lock, ReentrantLock and conditions Explicit locks add tryLock (with timeout), interruptible acquisition, fairness, multiple Conditions per lock and ReadWriteLock/StampedLock. Always lock(); try { … } finally { unlock(); }.

AtomicInteger/Long/Reference/… (CAS-based), LongAdder/LongAccumulator (striped counters for high contention — compared below), VarHandle for fine-grained memory modes.

volatile under the Java Memory Model A volatile field read/write is atomic and sequentially consistent; a write happens-before every subsequent read of that field — enough for flags and safe publication, not for count++ (Module 7).

Executors.newFixedThreadPool, newCachedThreadPool, newScheduledThreadPool, newVirtualThreadPerTaskExecutor (21), custom ThreadPoolExecutor (Module 15); AutoCloseable since 19.

Future and CompletableFuture Module 9: blocking get, composition with thenApply/thenCompose/thenCombine/allOf, error handling with exceptionally/handle, timeouts with orTimeout.

Fork/join and work stealing ForkJoinPool, RecursiveTask/RecursiveAction, the common pool behind parallel streams and CompletableFuture.*Async defaults (Module 15).

ConcurrentHashMap (lock-striped/CAS; atomic compute/merge/putIfAbsent), CopyOnWriteArrayList (snapshot iteration), ConcurrentLinkedQueue, BlockingQueue implementations, ConcurrentSkipListMap. Collections.synchronizedMap wraps every call in one lock — and compound operations on it still race.

Virtual threads and their version requirements Final in Java 21 (JEP 444). One virtual thread per task; blocking calls unmount it from its carrier. Pinning by synchronized in 21–23 (fixed in 24); don't pool virtual threads; limit concurrency to downstream resources with semaphores.

Structured concurrency status/version notes StructuredTaskScope is a preview API (JEP 453 in 21, revised in later JDKs) — --enable-preview required (Module 13). ScopedValue is also preview in 21.

Java NIO and asynchronous I/O Selector-based non-blocking channels (Netty), AsynchronousSocketChannel/AsynchronousFileChannel (completion handlers/futures), HttpClient.sendAsync (CompletableFuture).

Thread dumps and JFR jstack <pid>, jcmd <pid> Thread.print (detects deadlocks), jcmd <pid> Thread.dump_to_file -format=json (includes virtual threads), ThreadMXBean in code; Java Flight Recorder (-XX:StartFlightRecording, jcmd JFR.start) records lock contention, thread parking and virtual-thread pinning with low overhead; VisualVM and JDK Mission Control visualise them.

Key Terms

Per-object lock used by synchronized Explicit re-entrant lock with conditions and try-lock

Striped counter for high-contention increments Atomic read-modify-write on a map entry

JVM-scheduled lightweight thread (Java 21) Java Flight Recorder

Diagram or Execution Timeline

playground

Syntax and API Overview

synchronized, ReentrantLock Wait for condition wait/notifyAll in a loop, Condition.await/signalAll AtomicLong, LongAdder

ExecutorService, ForkJoinPool, virtual-thread executor

Complete Code Example

synchronized versus ReentrantLock with a Condition, AtomicLong versus LongAdder, atomic map updates, and virtual threads with a thread count (Temurin JDK 21; timings from one real run):

import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.concurrent.locks.*;

public class JavaToolkit {
    static final class BoundedBuffer {                       // explicit lock + two conditions
        private final Object[] items = new Object[4];
        private int head, tail, count;
        private final ReentrantLock lock = new ReentrantLock();
        private final Condition notFull = lock.newCondition(), notEmpty = lock.newCondition();

        void put(Object x) throws InterruptedException {
            lock.lock();
            try {
                while (count == items.length) notFull.await();  // predicate loop
                items[tail] = x; tail = (tail + 1) % items.length; count++;
                notEmpty.signal();
            } finally { lock.unlock(); }
        }
        Object take() throws InterruptedException {
            lock.lock();
            try {
                while (count == 0) notEmpty.await();
                Object x = items[head]; head = (head + 1) % items.length; count--;
                notFull.signal();
                return x;
            } finally { lock.unlock(); }
        }
    }

    static int syncCounte
…
ReentrantLock + Conditions bounded buffer: sum = 500500
synchronized counter: 400000
AtomicLong 8 threads x 2,000,000 increments = 16000000 in 321 ms
LongAdder  8 threads x 2,000,000 increments = 16000000 in 46 ms
ConcurrentHashMap.merge from 4 threads: {be=8, not=4, or=4, to=8}
20,000 virtual threads: peak concurrently sleeping = more than 10,000, platform threads in the JVM now = 16

Line-by-Line Explanation

The bounded buffer uses one ReentrantLock and two Conditions so producers and consumers wait on separate queues; await releases the lock while waiting; finally guarantees unlocking.

synchronized static method: all increments under the class monitor — exactly 400,000.

AtomicLong makes 8 threads CAS one cache line; LongAdder stripes the counter into cells per contending thread and sums on read — usually much faster under contention, at the cost of a non-atomic snapshot sum().

ConcurrentHashMap.merge is atomic per key: no lost counts (each of 4 threads adds "to"×2, "be"×2…).

Virtual threads: 20,000 tasks sleep concurrently; Thread.getAllStackTraces() lists only *platform* threads, which stay few.

Output

Shown above; counts are deterministic, timings vary.

Interleaving or State Trace

Bounded buffer when full (4 items): producer put(5) acquires the lock, sees count == 4, notFull.await() releases the lock and parks; consumer take() acquires, removes one, notFull.signal() wakes the producer, unlocks; producer re-acquires, re-checks count == 3, inserts.

Correctness and Safety Analysis

Shared state is protected per structure (buffer → its lock; counter → class monitor; map → per-key atomicity); results crossing threads are published by join, executor close() (which waits for termination) or atomics.

Common Mistakes and Misconceptions

if instead of while around wait()/await().

unlock() outside finally.

map.get then map.put on a ConcurrentHashMap (check-then-act) instead of merge/compute.

Pooling virtual threads or using them for CPU-bound work.

Calling blocking code inside CompletableFuture.supplyAsync on the common pool.

Failure Modes and Debugging Steps

Deadlock: jcmd <pid> Thread.print reports "Found one Java-level deadlock".

Contention: JFR "Java Monitor Blocked" and "Thread Park" events.

Virtual-thread pinning (21–23): -Djdk.tracePinnedThreads=full or JFR VirtualThreadPinned.

Thread leaks: thread dumps over time; name threads with factories.

Performance and Scalability Implications

LongAdder beats AtomicLong for hot counters; ConcurrentHashMap scales with fine-grained CAS; virtual threads remove thread-count limits for blocking I/O but not downstream limits.

Security Considerations

Thread-locals in pools can leak user context across requests; use ScopedValue (preview) or clear thread-locals; bound executors exposed to external input.

Platform and Runtime Caveats

Java 19+: ExecutorService is AutoCloseable. Java 21: virtual threads final; structured concurrency and scoped values preview. Java 24: synchronized no longer pins virtual threads.

Thread.stop, suspend, resume are removed/unsupported.

Real-World Applications

Spring/Tomcat request threads (virtual threads in Spring Boot 3.2+), Kafka clients, Netty-based servers (Vert.x, gRPC-Java), Elasticsearch thread pools, Android (with Kotlin coroutines on top).

Best Practices

Prefer java.util.concurrent over wait/notify.

Use ConcurrentHashMap atomic methods for compound updates.

Use virtual threads for blocking request handling; semaphores to bound downstream concurrency.

Name threads; capture thread dumps and JFR in production.

Exercises

E1 · Beginner. Why must Condition.await() be called in a while loop?

Answer: Spurious wake-ups and competing threads mean the condition may be false after waking; the loop re-checks before proceeding.

E2 · Intermediate. Rewrite if (!map.containsKey(k)) map.put(k, load(k)); for a ConcurrentHashMap so load runs at most once per key.

Answer: map.computeIfAbsent(k, key -> load(key)); — atomic per key (keep load short; it runs while the bin is locked).

E3 · Advanced challenge. A service moved to virtual threads and now overloads its database. Why, and what fixes it?

Answer: Virtual threads removed the implicit concurrency cap that a 200-thread pool provided, so thousands of requests now hit the database at once. Bound database concurrency explicitly (a connection pool with a wait timeout, or a Semaphore around queries).

Quiz

? What does a volatile write guarantee in Java?
+ It happens-before every subsequent read of the same field. :: Visibility and ordering.
- That count++ on the field is atomic. :: Read-modify-write still races.
- That the field is stored in a register. :: The opposite.
- Mutual exclusion. :: volatile is not a lock.

? Why is LongAdder faster than AtomicLong under heavy contention?
+ It spreads increments over several cells instead of one contended cache line. :: Striping.
- It skips synchronization. :: Each cell is still updated atomically.
- It uses virtual threads. :: Unrelated.
- It is wait-free while AtomicLong blocks. :: Neither blocks.

? Which are true in Java 21? (select all that apply)
+ Virtual threads are a final feature. :: JEP 444.
+ StructuredTaskScope requires --enable-preview. :: Preview API.
- synchronized never pins virtual threads. :: It does until Java 24.
+ ExecutorService can be used in try-with-resources. :: Since Java 19.

Summary

Java combines intrinsic monitors, explicit locks with conditions, atomics and adders, a precise memory model, executors and fork/join, CompletableFuture, high-quality concurrent collections, and since Java 21 virtual threads; structured concurrency is still a preview. Thread dumps and JFR make problems visible.

Further Reading

Brian Goetz et al., *Java Concurrency in Practice*.

JEP 444 (Virtual Threads), JEP 453/480/499 (Structured Concurrency previews), JEP 491.

Doug Lea, java.util.concurrent package documentation.

Oracle, "Java Flight Recorder" runtime guide.