Coordination, Locks, and Concurrency HazardsIntroductionConcurrent Java programs perform several activities at overlapping times. This improves responsiveness and throughput, but it also creates a central problem: threads may access the same mutable state at the same time. Without coordination, operations that appear simple—such as incrementing a counter, transferring money, or removing an item from a queue—may produce incorrect results. Java provides several coordination mechanisms. Intrinsic locks support basic mutual exclusion; ReentrantLock provides explicit locking and advanced policies; conditions allow threads to wait for state changes; semaphores control access to a limited number of resources; latches and barriers coordinate phases of work. These tools are powerful, but incorrect use can cause deadlock, livelock, starvation, excessive contention, and visibility errors. This chapter develops a practical design approach: identify shared state, establish a synchronization policy, choose the simplest adequate mechanism, release resources reliably, and test under realistic concurrency. Learning ObjectivesBy the end of this chapter, you should be able to: - explain mutual exclusion, visibility, atomicity, and happens-before relationships;
- use intrinsic locks and synchronized methods safely;
- apply
ReentrantLock, Condition, Semaphore, CountDownLatch, and CyclicBarrier; - implement producer–consumer communication correctly;
- distinguish deadlock, livelock, starvation, and contention;
- design lock ordering and timeout policies that reduce concurrency hazards;
- select an appropriate coordination mechanism for a practical Java application.
1. The Foundations of Thread Coordination1.1 Race conditions and compound actionsA race condition occurs when the result depends on the timing of threads. Consider: class Counter {
private int value;
void increment() {
value++; // read, add, write
}
int get() {
return value;
}
}The expression value++ is not one indivisible machine-level action. It consists conceptually of reading value, adding one, and writing the result. If two threads read the same old value, both may write the same new value, losing one update. A correct design must address three related properties: - Atomicity: an operation appears indivisible to other threads.
- Visibility: an update made by one thread becomes observable by another.
- Ordering: operations occur in an order consistent with the synchronization rules.
volatile provides visibility and ordering for a variable, but it does not generally make compound actions such as incrementing atomic. A lock can provide mutual exclusion, visibility, and ordering for a protected region.
1.2 Critical sections and invariantsA critical section is code that accesses shared mutable state and must not be executed concurrently with conflicting code. The protected state has an invariant—a condition that must remain true. For a bank account, the balance may be required never to fall below zero. For a bounded queue, the number of elements must remain between zero and its capacity. Synchronization should protect the invariant, not merely individual variables. Locking a balance update but not the corresponding transaction record may still leave the overall transaction state inconsistent. 1.3 The happens-before ideaJava’s memory model defines ordering relationships called happens-before relationships. If an action happens-before another action, the second action is entitled to observe the first action’s effects. Important examples include: - an unlock on a monitor happens-before a later lock on the same monitor;
- an unlock on a
Lock happens-before a later successful lock on that same object; - an action in a thread happens-before another thread successfully returns from
join() on it; - actions before
CountDownLatch.countDown() happen-before a thread successfully returns from await().
These relationships explain why synchronization is needed even when a data race appears unlikely. Timing is not a substitute for a memory-visibility guarantee. 2. Intrinsic Locks and the synchronized KeywordEvery Java object has an intrinsic lock, also called a monitor. A synchronized instance method locks this; a synchronized static method locks the Class object. A synchronized block can lock a chosen object: class Inventory {
private final Object lock = new Object();
private int units;
void add(int amount) {
if (amount < 0) throw new IllegalArgumentException("negative amount");
synchronized (lock) {
units += amount;
}
}
int available() {
synchronized (lock) {
return units;
}
}
}A private final lock object is often preferable to synchronizing on this, because external code cannot accidentally acquire the same lock and create undocumented coupling. Never use publicly accessible objects, interned strings, or mutable objects whose identity may change as lock objects. The lock is released automatically when the synchronized block exits, including through an exception. This makes intrinsic locking less vulnerable to the forgotten-unlock error associated with manual lock management. 2.1 Guarding conditions with wait, notify, and notifyAllAn intrinsic lock also supports communication through wait(), notify(), and notifyAll(). A waiting thread must own the monitor, and it must recheck the condition in a loop: class OneSlotBuffer<T> {
private T item;
public synchronized void put(T value) throws InterruptedException {
while (item != null) {
wait();
}
item = value;
notifyAll();
}
public synchronized T take() throws InterruptedException {
while (item == null) {
wait();
}
T result = item;
item = null;
notifyAll();
return result;
}
}The loop is essential because a thread can wake spuriously, because another thread may consume the condition before the awakened thread reacquires the lock, and because a notification does not itself mean that the required state is true. notifyAll() is usually safer than notify() when multiple condition types share one monitor, although it may wake more threads than necessary. 3. Explicit Locks with ReentrantLockReentrantLock offers capabilities not directly available with synchronized, including interruptible acquisition, timed acquisition, fairness configuration, and multiple condition objects.
import java.util.concurrent.locks.ReentrantLock;
class SafeCounter {
private final ReentrantLock lock = new ReentrantLock();
private int value;
void increment() {
lock.lock();
try {
value++;
} finally {
lock.unlock();
}
}
}The finally block is mandatory. If the critical section throws an exception and the lock is not released, other threads may wait indefinitely. Useful acquisition methods include: lock() waits until the lock is obtained;lockInterruptibly() allows a waiting thread to respond to interruption;tryLock() returns immediately with success or failure;tryLock(timeout, unit) waits for a bounded time.
A fair lock attempts to favor the longest-waiting thread. Fairness can reduce starvation but commonly lowers throughput because of increased scheduling and queue-management overhead. Fairness is therefore a policy choice, not an automatic performance improvement. 3.1 ConditionsA Condition is associated with a lock and represents a waiting condition. It is similar to an intrinsic monitor’s wait set, but one lock can have several condition queues: import java.util.concurrent.locks.*;
class BoundedBuffer<T> {
private final Object[] data;
private int head, tail, count;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
BoundedBuffer(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
data = new Object[capacity];
}
void put(T value) throws InterruptedException {
lock.lockInterruptibly();
try {
while (count == data.length) notFull.await();
data[tail] = value;
tail = (tail + 1) % data.length;
count++;
notEmpty.signal();
} finally {
lock.unlock();
}
}
@SuppressWarnings("unchecked")
T take() throws InterruptedException {
lock.lockInterruptibly();
try {
while (count == 0) notEmpty.await();
T value = (T) data[head];
data[head] = null;
head = (head + 1) % data.length;
count--;
notFull.signal();
return value;
} finally {
lock.unlock();
}
}
}await() atomically releases the lock and suspends the thread; before returning, it reacquires the lock. As with wait(), the condition must be checked in a loop. signal() wakes one waiter; signalAll() wakes all waiters. The choice depends on whether one awakened thread is sufficient and whether different waiting predicates may be involved.
4. Semaphores: Controlling a Resource PoolA semaphore maintains a number of permits. A thread acquires a permit before using a limited resource and releases it afterward. Unlike a mutual-exclusion lock, a semaphore may allow several threads to enter simultaneously. import java.util.concurrent.Semaphore;
class ConnectionPool {
private final Semaphore permits = new Semaphore(10, true);
void useConnection() throws InterruptedException {
permits.acquire();
try {
// obtain and use one of at most ten connections
} finally {
permits.release();
}
}
}The number of permits should represent the actual capacity of the resource, not an arbitrary desired concurrency level. Releasing a permit in a finally block prevents capacity leakage. A semaphore can also implement mutual exclusion with one permit, but Lock or synchronized communicates that intent more clearly. Semaphores are useful for database connection pools, API rate limits, bounded file processing, and admission control. They do not automatically protect the internal consistency of the resource; the resource itself may still require a lock. 5. Latches and Barriers5.1 CountDownLatchA CountDownLatch has a count that only decreases. Threads calling await() wait until the count reaches zero. It is appropriate for one-time events, such as waiting for several services to initialize: CountDownLatch ready = new CountDownLatch(3);
Runnable worker = () -> {
try {
initializeService();
} finally {
ready.countDown();
}
};
// start three workers
ready.await();
startApplication();The latch cannot be reset. If repeated phases are needed, use a barrier or another coordination design. 5.2 CyclicBarrierA CyclicBarrier allows a fixed number of threads to meet repeatedly at a common point: CyclicBarrier barrier = new CyclicBarrier(4, () ->
System.out.println("Phase completed"));
for (int phase = 0; phase < 5; phase++) {
performPhase(phase);
barrier.await();
}The barrier action runs when the last participating thread arrives. If one thread leaves early, times out, or is interrupted, the barrier may become broken and waiting threads receive BrokenBarrierException. Production code must define how to cancel or restart the computation when this occurs. A latch coordinates an event that happens once; a barrier coordinates repeated rendezvous among a fixed group. | Mechanism | Main purpose | Reusable? | Typical example |
|---|
synchronized | Mutual exclusion and visibility | Yes | Protect object invariant | ReentrantLock | Explicit, flexible locking | Yes | Timed or interruptible lock acquisition | Condition | Wait for a predicate | Yes | Bounded buffer | Semaphore | Limit concurrent access | Yes | Connection pool | CountDownLatch | Wait for one event or group | No | Startup readiness | CyclicBarrier | Meet at repeated phases | Yes | Parallel simulation |
6. Communication and InterruptionGood concurrent programs communicate state changes rather than repeatedly polling shared variables. Blocking queues are often preferable to implementing producer–consumer communication manually: BlockingQueue<String> jobs = new ArrayBlockingQueue<>(100);
jobs.put("compile"); // waits if full
String job = jobs.take(); // waits if emptyput() and take() combine storage, locking, waiting, and notification correctly. Higher-level utilities such as ExecutorService, Future, and CompletableFuture should also be preferred when they express the problem directly.
Interruption is a cooperative cancellation mechanism. A method that catches InterruptedException should usually restore the interrupt status if it cannot propagate the exception: try {
queue.take();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return;
}Silently swallowing interruption can prevent an application from shutting down promptly. Every blocking operation should have a documented interruption and timeout policy. 7. Concurrency Hazards7.1 DeadlockDeadlock occurs when threads wait forever for resources held by one another. Four conditions are traditionally associated with deadlock: mutual exclusion, hold and wait, no pre-emption, and circular wait. Removing any one condition can prevent deadlock. A common example is inconsistent lock order: // Thread A: locks account 1, then account 2
// Thread B: locks account 2, then account 1 If each thread acquires its first lock before the other can proceed, both wait indefinitely. The strongest practical prevention technique is a global lock order. For account transfers, assign each account a stable numerical identifier and always lock the lower identifier first. Other techniques include acquiring all required locks together, using timed tryLock() with rollback, reducing nested locking, and using immutable messages instead of shared state. 7.2 LivelockIn livelock, threads remain active but make no useful progress. For example, two polite threads may repeatedly release a resource whenever they detect that the other is waiting, then collide again. Randomized back-off, priority rules, and bounded retries can help. Unlike deadlock, CPU usage may remain high during livelock. 7.3 Starvation and unfairnessStarvation occurs when a thread is repeatedly denied the resources it needs. Causes include unfair lock acquisition, long critical sections, priority misuse, and a producer that continually fills a resource before another consumer can access it. Fair locks and fair semaphores may reduce starvation, but fairness can reduce throughput. A sound design identifies which guarantee is required: safety, progress, bounded waiting, or maximum throughput. 7.4 Contention and lock granularityContention occurs when many threads compete for the same lock. A single global lock is simple but may serialize unrelated operations. Fine-grained locks can improve parallelism but increase complexity and deadlock risk. The correct balance depends on workload, critical-section duration, and the cost of coordination. Keep critical sections short. Do not perform network calls, user callbacks, file operations, or unpredictable blocking work while holding a lock unless the design explicitly requires it. Lock profiling and stress testing are more reliable than guessing. 7.5 Unsafe publication and visibility errorsAn object may be incorrectly published if another thread can observe it before construction is complete. Use safe publication techniques such as static initialization, final fields used correctly, synchronized access, volatile references, or concurrent collections. A ConcurrentHashMap protects its own operations, but it does not make an arbitrary multi-step sequence atomic. For example, “check then act” may still require computeIfAbsent, a lock, or another atomic operation. 8. A Practical Design MethodA disciplined workflow reduces concurrency defects: - Identify shared mutable state. Classify each field as thread-confined, immutable, safely published, or guarded.
- Write the invariant. State what must remain true before and after each operation.
- Choose ownership. Prefer one component responsible for modifying a piece of state.
- Select the simplest mechanism. Use concurrent collections or blocking queues before custom coordination.
- Define lock scope and order. Document which lock guards which fields and the order for nested locks.
- Specify waiting behavior. Decide whether operations block, time out, or respond to interruption.
- Plan shutdown. Stop producers, interrupt workers, close resources, and await termination.
- Test adverse schedules. Repeat tests, inject delays, use thread dumps, and test cancellation and timeouts.
Applied Case: A Concurrent Ticket-Booking ServiceBackgroundConsider an illustrative ticket-booking service with a fixed number of seats, several booking threads, and a reporting thread. Two customers must never receive the same seat, and the service must not allow more than 100 simultaneous database connections. ProblemA naïve implementation checks the number of available seats and decrements it in separate unsynchronized statements. Under load, two requests may observe the same availability. A second problem occurs when the service uses one database connection per booking but does not restrict concurrent acquisition. AnalysisSeat allocation is a shared invariant: allocated seats plus available seats must equal the configured capacity. The check and allocation must therefore be one atomic operation. Database connections are a bounded external resource and require admission control rather than exclusive access to all booking requests. Approach and solutionA private ReentrantLock protects the seat map. A fair Semaphore with 100 permits limits database work. The service acquires the semaphore first, then performs the short seat transaction under the seat lock, and releases both resources in reverse order. All lock and permit releases occur in finally blocks. A timeout on semaphore acquisition allows the service to return a controlled “busy” response rather than accumulating unlimited waiting requests. If two locks become necessary—for example, a seat lock and a customer lock—the service defines and follows one global order. It never calls an external payment provider while holding the seat lock; instead, it records a reservation state, releases the lock, performs payment, and then completes or cancels the reservation through another synchronized transition. OutcomeThe design prevents duplicate allocation, bounds database pressure, and avoids holding internal locks during slow external calls. Timeouts and interruption support graceful shutdown. Metrics should record lock wait time, semaphore wait time, booking latency, rejected requests, and cancellation rates. Learning pointsThe case illustrates that coordination is a design problem, not merely a keyword choice. Locks protect invariants, semaphores protect capacity, timeouts protect responsiveness, and explicit state transitions prevent partially completed operations from being mistaken for successful bookings. Limitations, Governance, and Contemporary PracticeSynchronization does not solve every concurrent-programming problem. Excessive locking can reduce performance; timeouts can produce partial operations; fair policies can reduce throughput; and a correct local lock may not protect state distributed across multiple processes or services. Distributed coordination requires different tools such as transactions, leases, or consensus protocols. Concurrency also has governance implications. A service should expose bounded resource use, avoid denial-of-service through unbounded queues, protect sensitive data from races, and record enough telemetry to investigate failures without logging confidential information. Testing should include cancellation, overload, process termination, and recovery after partial failure. Modern Java development commonly favors high-level concurrency utilities, immutable data, structured task management where available, and virtual threads for large numbers of blocking tasks. Virtual threads reduce the cost of blocking but do not remove data races, deadlocks, or the need for bounded external resources. An application can create many virtual threads while still requiring a semaphore to limit database connections. Best-Practice Checklist- Protect complete invariants, not isolated fields.
- Prefer immutable objects and thread confinement.
- Use
synchronized when simple mutual exclusion is sufficient. - Use
ReentrantLock only when its additional capabilities are needed. - Always unlock or release permits in
finally blocks. - Await conditions in loops.
- Establish and document a global lock order.
- Use timeouts and interruption for operations that may wait indefinitely.
- Prefer
BlockingQueue and concurrent collections to custom protocols. - Keep critical sections short and avoid external calls while holding locks.
- Test under load and inspect thread dumps for blocked threads.
- Treat interruption as a meaningful cancellation request.
Chapter SummaryJava coordination mechanisms provide different guarantees. Intrinsic locks and ReentrantLock protect critical sections and establish visibility. Conditions allow threads to wait for predicates, while semaphores limit access to finite resources. Latches coordinate one-time readiness events, and barriers synchronize repeated phases. Blocking queues and higher-level concurrency utilities often provide safer implementations than custom wait/notify protocols. Concurrency hazards arise when resource ownership, waiting, or visibility is poorly designed. Deadlock is prevented through lock ordering, reduced nesting, timed acquisition, and careful resource management. Livelock requires progress policies and back-off; starvation requires attention to fairness and bounded waiting; contention requires appropriate granularity and short critical sections. Professional concurrent software combines correct synchronization with interruption, timeouts, safe publication, testing, observability, and a clear shutdown strategy. Key TermsRace condition: A result that depends on unpredictable thread timing. Atomicity: The property that an operation appears indivisible. Visibility: The ability of one thread to observe another thread’s updates. Happens-before: A Java Memory Model ordering relationship that guarantees visibility and ordering. Critical section: Code that accesses shared state and requires coordinated execution. Invariant: A condition that must remain true for a component to be correct. Intrinsic lock: The monitor lock associated with every Java object. Monitor: The synchronization mechanism used by intrinsic locking. ReentrantLock: An explicit lock that the owning thread may acquire repeatedly. Condition: A wait set associated with an explicit lock and a state predicate. Semaphore: A synchronizer containing a set of permits. CountDownLatch: A one-use synchronizer that opens when its count reaches zero. CyclicBarrier: A reusable rendezvous point for a fixed group of threads. Deadlock: Permanent waiting caused by cyclic resource dependencies. Livelock: Active execution without useful progress. Starvation: Indefinite denial of access to a needed resource. Contention: Competition among threads for the same resource. Safe publication: Making an object visible to other threads in a correctly initialized state. Back-off: Delaying and retrying after a conflict to reduce repeated collisions. Review QuestionsShort Answer Questions- Why is
value++ not generally an atomic operation? - What is the difference between a
CountDownLatch and a CyclicBarrier? - Why should
await() and wait() normally be used inside a loop? - What is the purpose of a semaphore in a connection pool?
- Why must an explicit lock be released in a
finally block?
Long Answer Questions- Explain atomicity, visibility, and ordering, and relate each to Java synchronization.
- Compare intrinsic locks and
ReentrantLock, including their advantages and limitations. - Describe deadlock and explain at least four prevention strategies.
- Explain how conditions implement a bounded buffer and why separate conditions can be useful.
- Discuss how lock granularity, fairness, and critical-section length affect performance and correctness.
Analytical / Application Questions- Design a coordination strategy for a system with 20 worker threads and only five external-service connections. Justify the use of each synchronizer.
- Two methods acquire locks in opposite order and occasionally freeze the application. Diagnose the hazard and propose a corrected design.
- A service uses a large unbounded queue and never responds to interruption. Analyze the risks and recommend changes for overload control and graceful shutdown.
Multiple Choice Questions with AnswersQuestion 1Which property ensures that a compound operation appears indivisible to other threads? A. Visibility B. Atomicity C. Fairness D. Publication Correct Answer: B. Atomicity Question 2What is the principal purpose of a Semaphore? A. To coordinate repeated phases B. To provide object serialization C. To limit concurrent access to a finite resource D. To replace all interruption handling Correct Answer: C. To limit concurrent access to a finite resource Question 3Which mechanism is designed for a one-time event and cannot be reset? A. CountDownLatch B. CyclicBarrier C. Condition D. ReentrantLock Correct Answer: A. CountDownLatch Question 4Why should code waiting on a Condition recheck its predicate in a loop? A. Conditions automatically release all locks permanently B. Wake-ups do not guarantee that the predicate is true C. The loop makes the lock fair D. A loop converts the condition into a semaphore Correct Answer: B. Wake-ups do not guarantee that the predicate is true Question 5Which practice most directly prevents deadlock caused by opposite lock order? A. Using longer critical sections B. Applying a consistent global lock order C. Removing all timeouts D. Using an unbounded queue Correct Answer: B. Applying a consistent global lock order Question 6What must normally happen when a method catches InterruptedException but cannot propagate it? A. The interrupt status should be restored B. The exception should always be ignored C. All locks should be made fair D. The thread should be restarted automatically Correct Answer: A. The interrupt status should be restored Question 7Which mechanism is most appropriate for repeated synchronization of a fixed group at phase boundaries? A. CountDownLatch B. CyclicBarrier C. Semaphore with zero permits D. AtomicInteger Correct Answer: B. CyclicBarrier Question 8What is a major risk of performing a network call while holding a lock? A. The call becomes automatically atomic B. The lock is converted into a semaphore C. The critical section may remain occupied for an unpredictable time D. Visibility guarantees are removed Correct Answer: C. The critical section may remain occupied for an unpredictable time Question 9What does a fair ReentrantLock generally attempt to reduce? A. Object creation B. Starvation C. Exception handling D. Memory visibility Correct Answer: B. Starvation Question 10Which Java facility is usually preferable for standard producer–consumer communication? A. A manually shared integer flag B. A BlockingQueue C. An unsynchronized ArrayList D. A public String used as a lock Correct Answer: B. A BlockingQueue
|