Concurrency and Asynchronous ProgrammingIntroductionA sequential Java program executes one operation at a time. Concurrency allows multiple tasks to make progress during overlapping periods, while parallelism means that tasks literally execute simultaneously on different processors or cores. The distinction matters: a single-core system may provide concurrency through time-sharing, whereas a multicore system may provide both concurrency and parallelism. Concurrency is essential in web servers, mobile applications, databases, scientific systems, games, and cloud services. A server may handle thousands of network requests while individual requests wait for files, databases, or remote services. Correct concurrent programming is difficult because execution order is nondeterministic. Programs must therefore establish rules for shared data, communication, cancellation, and failure. This chapter develops Java’s principal concurrency abstractions: threads, synchronization, locks, atomic variables, executors, futures, concurrent collections, virtual threads, and structured concurrency concepts. It also examines deadlock, visibility, atomicity, thread safety, and design practices. Learning ObjectivesBy the end of this chapter, students should be able to: - Distinguish concurrency, parallelism, asynchronous execution, and multitasking.
- Explain thread states, scheduling, interruption, atomicity, visibility, and ordering.
- Design synchronized and lock-based thread-safe classes.
- Identify and prevent race conditions and deadlocks.
- Select suitable executors, futures, completion stages, and concurrent collections.
- Explain the advantages and constraints of virtual threads.
- Apply structured-concurrency principles to reliable task decomposition.
- Evaluate a concurrent design for correctness, performance, cancellation, and maintainability.
1. The Conceptual FoundationA process is an executing program with its own address space. A thread is an execution path within a process. Threads in one Java Virtual Machine share heap objects but have separate stacks and program counters. Shared memory makes communication efficient, but it also creates the possibility of interference. Concurrency concerns the composition of independently progressing tasks. Parallelism concerns simultaneous execution. Asynchronous programming separates initiating an operation from receiving its result; the initiating thread need not wait immediately. These concepts can occur together but are not synonyms. A useful model is a task consisting of computation, waiting, and communication. If a task spends substantial time waiting for I/O, many tasks can share a limited number of platform threads. If a task is CPU-bound, additional threads help only up to the available parallel processing capacity; excessive threads create context-switching overhead. Java applications should generally express what work is required separately from which threads execute it. The java.util.concurrent package supports this separation. 2. Threads and Their Life CycleA Java thread can be created directly: Thread t = new Thread(() -> System.out.println("Running"));
t.start();Calling start() schedules a new execution path. Calling run() directly does not create a new thread; it simply invokes a method in the current thread. A thread commonly passes through these states: | State | Meaning |
|---|
| NEW | Created but not started | | RUNNABLE | Eligible to run, possibly currently running | | BLOCKED | Waiting to acquire an intrinsic monitor | | WAITING | Waiting indefinitely for another action | | TIMED_WAITING | Waiting for a bounded time | | TERMINATED | Execution has completed |
The scheduler does not guarantee a particular order among runnable threads. Therefore, correctness must not depend on timing, thread priority, or an observed execution sequence. Interruption is Java’s cooperative cancellation mechanism. interrupt() sets a thread’s interruption status and may cause blocking methods such as sleep() or wait() to throw InterruptedException. A method that cannot meaningfully handle interruption should usually restore the status: try {
queue.take();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return;
}Interruption is a request, not forced termination. Code should periodically respond to it and release resources in finally blocks. 3. The Three Core Correctness Properties3.1 AtomicityAn operation is atomic when it appears indivisible to other threads. The expression count++ is not atomic: it reads count, adds one, and writes the result. Two threads can read the same old value and overwrite one another’s update. This is a lost-update race condition. 3.2 VisibilityVisibility means that one thread eventually observes another thread’s completed write. Without an appropriate happens-before relationship, a thread may repeatedly observe a stale value because of processor caches, compiler transformations, or register allocation. The volatile keyword provides visibility and ordering for a variable, but it does not generally make compound actions atomic. This is safe: private volatile boolean shutdown; This is not made safe merely by volatile: volatile int count;
count++; // still a read-modify-write race 3.3 Ordering and Happens-BeforeThe Java Memory Model defines happens-before relationships. If action A happens-before action B, B must observe the effects required by the memory model. Important examples include: - An unlock on a monitor happens-before a later lock on that monitor.
- A write to a volatile variable happens-before a later read of that variable.
- Actions before
Thread.start() happen-before actions in the started thread. - Actions in a thread happen-before another thread successfully returns from
join(). - Completion of a task submitted to an executor is coordinated with retrieval of its result through the executor/future mechanisms.
These rules explain why synchronization is more than mutual exclusion: it also establishes memory visibility. 4. Intrinsic SynchronizationEvery Java object has an intrinsic monitor. A synchronized method or block acquires that monitor, executes its protected code, and releases it even when an exception occurs. public final class Counter {
private int value;
public synchronized void increment() {
value++;
}
public synchronized int get() {
return value;
}
}Only one thread at a time can execute synchronized sections guarded by the same monitor. The monitor also creates visibility guarantees. A synchronized block can make the protection boundary explicit: private final Object lock = new Object();
private int balance;
public void deposit(int amount) {
if (amount < 0) throw new IllegalArgumentException();
synchronized (lock) {
balance += amount;
}
}Private lock objects prevent unrelated code from accidentally using the same monitor. Synchronizing on public objects, strings, or classes can create unexpected contention or deadlocks. A thread-safe class must protect all access to mutable shared state, not merely selected methods. Public methods should preserve invariants. For example, a bank account should ensure that validation and balance modification occur under the same protection if another thread could intervene between them. 5. Explicit Locks and ConditionsReentrantLock provides capabilities not available directly with synchronized, including timed acquisition, interruptible acquisition, and optional fairness.
private final Lock lock = new ReentrantLock();
public void update() {
lock.lock();
try {
// protected state transition
} finally {
lock.unlock();
}
}The finally block is essential. Failure to unlock can permanently block other threads. tryLock() supports bounded waiting:
if (lock.tryLock()) {
try {
updateState();
} finally {
lock.unlock();
}
} else {
deferWork();
}A Condition associated with a lock supports waiting for a logical condition, such as “buffer is not empty.” Always test the condition in a while loop because wake-ups can be spurious and because another thread may change the state before the awakened thread reacquires the lock. while (buffer.isEmpty()) {
notEmpty.await();
}ReadWriteLock can allow multiple readers but only one writer. It is useful when reads greatly outnumber writes and the protected operations are not too short. Otherwise, its bookkeeping may cost more than a simple lock.
6. Atomic Variables and ImmutabilityThe java.util.concurrent.atomic package supplies lock-free or highly optimized operations for selected variables. For example: private final AtomicInteger requests = new AtomicInteger();
requests.incrementAndGet(); Atomic classes support compare-and-set (CAS): update a value only if it still equals an expected value. AtomicReference is useful for safely replacing an immutable object reference. LongAdder is often preferable to AtomicLong for highly contended counters where an exact instantaneous value is not required during updates. Atomicity of one variable does not automatically make a multi-variable invariant safe. If a transfer must decrease one account and increase another as one logical action, two independent atomic variables are insufficient; a common lock or transactional design is required. Immutability is a powerful alternative. An immutable object has no state that changes after construction. If its fields are properly initialized and references do not escape unsafely, it can be shared among threads without synchronization for its internal state. Records can assist with immutable data carriers, although a record containing a mutable list is not automatically deeply immutable. 7. Deadlock, Starvation, and LivelockA deadlock occurs when threads wait forever for one another. The classic necessary conditions are mutual exclusion, hold-and-wait, no preemption, and circular wait. synchronized (first) {
synchronized (second) {
transfer();
}
}If another code path locks second and then first, two threads can form a cycle. Prevention techniques include: - Establish and document a global lock order.
- Acquire multiple locks in that order.
- Keep critical sections short.
- Avoid calling external or unknown code while holding a lock.
- Use timed
tryLock() and recover when acquisition fails. - Prefer higher-level concurrent abstractions.
Starvation occurs when a thread cannot obtain resources because others continually take them. Livelock occurs when threads remain active but repeatedly react to one another without completing useful work. Fairness policies and randomized backoff can help, but they may reduce throughput. Thread dumps, Java Flight Recorder, profilers, and executor metrics help diagnose blocked or overloaded systems. Testing should include stress, repetition, timeouts, and race detectors where available; a test that passes once does not prove concurrency correctness. 8. Executors and Task-Based DesignCreating one platform thread per request can exhaust memory and scheduling capacity. An ExecutorService manages worker threads and accepts tasks: ExecutorService pool = Executors.newFixedThreadPool(8);
try {
pool.submit(() -> process(item));
} finally {
pool.shutdown();
}The executor’s size should reflect workload. CPU-bound work generally benefits from a pool near the number of available processors. I/O-bound work may use more threads, but the appropriate value depends on latency, memory, downstream limits, and measurement. An unbounded queue can conceal overload; bounded queues and explicit rejection policies provide backpressure. Common executor forms include fixed pools, scheduled executors, work-stealing pools, and virtual-thread-per-task executors. The choice should follow task characteristics rather than convenience. Executor ownership must be clear: the component that creates a pool should normally shut it down. 9. Futures and Asynchronous CompositionA Future<V> represents a result that may become available later. get() blocks until completion, while cancel(true) requests cancellation. A CompletableFuture<T> supports nonblocking composition: CompletableFuture<User> user = fetchUserAsync(id);
CompletableFuture<Profile> profile = user.thenCompose(this::fetchProfile);
profile.thenAccept(this::display)
.exceptionally(error -> { log(error); return null; });Important distinctions include: thenApply transforms a value synchronously in the completion pipeline.thenCompose flattens an asynchronous operation and avoids nested futures.thenCombine combines two independent results.allOf waits for multiple futures but does not itself contain their typed results.
Asynchronous APIs still require an execution context. Accidentally running blocking operations on a common pool can delay unrelated work. Exceptions should be handled explicitly, cancellation should propagate where possible, and timeouts should protect callers from indefinitely slow dependencies. 10. Concurrent Collections and Coordination ToolsOrdinary collections such as ArrayList and HashMap are not safe for unsynchronized concurrent mutation. Java provides specialized alternatives: | Tool | Appropriate use |
|---|
ConcurrentHashMap | Concurrent key-value access and atomic map operations | CopyOnWriteArrayList | Many reads, very few writes | BlockingQueue | Producer-consumer pipelines | ConcurrentLinkedQueue | Nonblocking queue operations | Semaphore | Limiting concurrent access to a resource | CountDownLatch | One-time waiting for several events | CyclicBarrier | Repeated phase synchronization | Phaser | Flexible multi-phase coordination |
Compound map actions should use atomic methods such as computeIfAbsent, rather than separate “check then put” operations. A BlockingQueue naturally expresses backpressure: producers wait or fail when capacity is unavailable, and consumers wait when no item exists. 11. Virtual Threads and Structured ConcurrencyPlatform threads are operating-system resources and are relatively expensive. Virtual threads, finalized as a standard feature in modern Java releases, are lightweight JVM-managed threads designed for high-concurrency, mostly blocking workloads. They make thread-per-request programming practical: try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<String> result = executor.submit(() -> callService());
System.out.println(result.get());
}Virtual threads improve scalability for waiting tasks; they do not make CPU work faster and do not eliminate synchronization problems. Applications must still limit scarce resources such as database connections. Long blocking operations inside synchronized sections, native calls, or pinning situations can reduce scalability. Profiling remains necessary. Structured concurrency is a design concept in which child tasks have a lexical lifetime bounded by a parent operation. The parent starts related subtasks, waits for them, propagates failure or cancellation, and does not return while children continue invisibly. This improves ownership, observability, and error handling. Java’s structured-concurrency APIs have evolved through preview stages, so production use must be checked against the target JDK and its documentation. The principle remains valuable even when implemented with scoped executor management and explicit future coordination. 12. Design Method for Thread-Safe SystemsA disciplined design process is more reliable than adding locks after defects appear. - Identify ownership. Decide which component owns each mutable object.
- Minimize sharing. Prefer local variables, immutable values, message passing, and confinement.
- State invariants. For example, a queue’s size cannot be negative and a balance cannot change without a validated transaction.
- Choose one synchronization policy. Document which lock, actor, queue, or atomic mechanism protects each state element.
- Define lifecycle. Specify startup, shutdown, cancellation, timeout, and failure behavior.
- Bound resources. Limit queues, connections, retries, and concurrent requests.
- Test adverse schedules. Exercise delays, interruption, overload, and partial failure.
- Measure. Inspect throughput, latency, contention, queue depth, memory, and rejected tasks.
A thread-safe component should also define whether returned collections are immutable snapshots, live concurrent views, or unsafe references. Exposing internal mutable state defeats otherwise correct synchronization. Applied Case: Illustrative Order-Processing ServiceBackgroundAn online retailer receives orders through an HTTP service. Each order requires validation, inventory reservation, payment authorization, and confirmation. Inventory and payment services are slower than local computation and can fail independently. ProblemAn initial implementation creates an unbounded number of platform threads and updates inventory using a check followed by a decrement. Under load, two orders can both observe the same stock and oversell it. Slow payment calls also exhaust threads, causing timeouts. AnalysisThe inventory reservation is a compound state transition. It requires atomicity around “stock is sufficient” and “stock decreases.” The service also needs bounded concurrency for payment calls, cancellation when a client disconnects, and reliable cleanup during shutdown. ApproachInventory is protected by a lock or implemented with an atomic conditional update. For example, a reservation method executes the validation and decrement under one synchronization policy. A bounded executor controls CPU-heavy validation, while a virtual-thread-per-task executor handles many mostly blocking request workflows. A semaphore limits simultaneous payment calls to the number supported by the payment provider. Futures or structured task scopes coordinate independent validation and fraud checks; if a required task fails, related work is cancelled. The service uses a bounded queue where appropriate, explicit timeouts, idempotency keys for retry-safe payment requests, and metrics for queue depth, reservation failures, latency, and cancellation. Shutdown stops accepting work, prevents new submissions, waits for active tasks, and records unfinished operations for recovery. OutcomeThe design prevents overselling, avoids unlimited thread creation, and makes overload visible through bounded resources. It does not assume that virtual threads solve external bottlenecks: the payment provider and database remain explicitly protected. Learning PointsConcurrency correctness depends on invariants and ownership, not merely on using a modern API. Atomic state transitions, bounded resources, cancellation, idempotency, and observability must be designed together. Challenges, Governance, and Professional PracticeConcurrency defects can corrupt financial records, expose private data, duplicate payments, or cause denial of service. Systems handling personal or financial information should maintain audit trails, least-privilege access, deterministic recovery procedures, and documented failure semantics. Logs must avoid sensitive data while retaining correlation identifiers needed for diagnosis. Developers should treat retries carefully: repeating a non-idempotent operation may create duplicate effects. Timeouts do not necessarily stop remote work, so cancellation and provider-side idempotency are important. Dependency limits, rate limits, and circuit breakers complement Java synchronization but do not replace it. AI-assisted code generation can suggest executors or synchronization patterns, but generated concurrent code requires review against the Java Memory Model, API version, workload, and failure behavior. Benchmark results should be reproducible and representative; microbenchmarks can mislead because the JVM optimizes code dynamically. Chapter SummaryConcurrency allows overlapping progress; parallelism performs work simultaneously. Java threads share heap state, so thread-safe designs must address atomicity, visibility, and ordering. synchronized, explicit locks, atomic variables, immutability, and concurrent collections provide different trade-offs. Volatile variables provide visibility but do not make compound operations atomic. Deadlocks arise from cyclic resource waiting and can be reduced through lock ordering, short critical sections, and higher-level abstractions. Executors separate task submission from thread management, while futures and completion stages compose asynchronous results. Blocking queues and coordination utilities support producer-consumer and phased designs. Virtual threads support large numbers of mostly blocking tasks but do not remove CPU, resource, or synchronization limits. Structured concurrency emphasizes bounded task lifetimes, failure propagation, and cancellation. Professional systems combine these mechanisms with ownership, bounded resources, timeouts, idempotency, testing, monitoring, and secure governance. Key TermsThread: An execution path within a process. Concurrency: Overlapping progress of multiple tasks. Parallelism: Simultaneous execution of tasks. Asynchronous programming: Separating operation initiation from immediate result retrieval. Race condition: A defect caused by timing-dependent access to shared state. Atomicity: The property that an operation appears indivisible. Visibility: The property that one thread observes another thread’s write. Happens-before: A Java Memory Model ordering relationship guaranteeing required visibility. Monitor: The intrinsic lock associated with a Java object. Volatile: A variable modifier providing visibility and ordering, but not general compound-operation atomicity. Deadlock: Permanent waiting caused by a cycle of resource dependencies. Starvation: Failure to obtain resources because other tasks continually take them. Executor: An abstraction that accepts tasks and manages their execution. Future: A handle representing a possibly incomplete result. Concurrent collection: A collection designed for safe concurrent access. Backpressure: Regulation that prevents producers from overwhelming consumers. Virtual thread: A lightweight JVM-managed thread. Structured concurrency: Organization of child tasks within a bounded parent lifetime. Idempotency: The property that repeating an operation has the same intended effect as performing it once. Review QuestionsShort Answer Questions- Distinguish concurrency from parallelism.
- Why does
volatile not make count++ safe? - What is the purpose of restoring interruption status?
- Name two techniques for preventing deadlock.
- When is a
BlockingQueue useful?
Long Answer Questions- Explain atomicity, visibility, and happens-before using a thread-safe counter as an example.
- Compare intrinsic synchronization,
ReentrantLock, and atomic variables. - Explain how executor sizing should differ for CPU-bound and I/O-bound tasks.
- Describe the benefits and limitations of virtual threads.
- Explain structured concurrency and its relationship to cancellation and failure propagation.
Analytical / Application Questions- Two methods acquire locks in opposite orders. Analyze the possible failure and redesign the locking policy.
- Design a bounded producer-consumer system for image processing, specifying its queue, executor, shutdown, and overload behavior.
- An order service oversells inventory during load testing. Identify the race condition and propose a synchronization and idempotency strategy.
Multiple Choice Questions with AnswersQuestion 1Which statement best describes the effect of calling run() directly on a Thread object? A. It creates a new operating-system thread. B. It executes the method in the current thread. C. It permanently blocks the current thread. D. It automatically creates a virtual thread. Correct Answer: B. It executes the method in the current thread. Question 2Which property is provided by volatile access but not generally by a compound increment operation? A. Visibility B. Mutual exclusion C. Transaction rollback D. Deadlock prevention Correct Answer: A. Visibility Question 3What is the principal reason for using a finally block with ReentrantLock? A. To increase thread priority B. To guarantee that the lock is released C. To convert the lock into a monitor D. To make all fields immutable Correct Answer: B. To guarantee that the lock is released Question 4Which situation is a deadlock? A. A task completes after a timeout. B. A thread is interrupted during sleep. C. Two threads wait forever for locks held by each other. D. A queue rejects a task when full. Correct Answer: C. Two threads wait forever for locks held by each other. Question 5Which method is most appropriate for atomically computing a missing value in a ConcurrentHashMap? A. computeIfAbsent B. containsKey followed by put C. size followed by clear D. values followed by remove Correct Answer: A. computeIfAbsent Question 6What is a major use of a BlockingQueue? A. Encrypting messages B. Coordinating producer-consumer workflows C. Replacing every database transaction D. Guaranteeing parallel execution Correct Answer: B. Coordinating producer-consumer workflows Question 7Which statement about virtual threads is correct? A. They make CPU-bound algorithms inherently faster. B. They eliminate the need to limit database connections. C. They are lightweight and well suited to many mostly blocking tasks. D. They prevent all race conditions. Correct Answer: C. They are lightweight and well suited to many mostly blocking tasks. Question 8Why should a condition associated with a lock usually be tested in a while loop? A. Locks cannot be used in if statements. B. Wake-ups can be spurious or the condition may change before reacquisition. C. The loop increases processor speed. D. It converts a checked exception into an unchecked exception. Correct Answer: B. Wake-ups can be spurious or the condition may change before reacquisition. Question 9Which practice most directly reduces lock-order deadlocks? A. Acquiring locks in a globally consistent order B. Using an unbounded task queue C. Increasing every thread’s priority D. Calling external services while holding locks Correct Answer: A. Acquiring locks in a globally consistent order Question 10What is a central principle of structured concurrency? A. Child tasks outlive their parent operation by default. B. Related child tasks have a bounded lifetime managed by a parent. C. Every task must use a global thread pool. D. Exceptions are ignored until application shutdown. Correct Answer: B. Related child tasks have a bounded lifetime managed by a parent.
|