Advanced Concurrency
Thread safety, concurrent collections, deadlock, livelock and starvation
Advanced Concurrency
Definition
Advanced concurrency in Java is about designing systems that remain correct, diagnosable, and performant when many threads interact with shared state. The topics go beyond primitive locking and cover thread safety strategies, concurrent collections, safe publication, non-blocking counters, and failure modes such as deadlock, livelock, and starvation. At this level, the question is no longer “how do I protect one variable?” but “how do I shape ownership so the system scales without becoming impossible to reason about?”
The advanced view is architectural. Concurrency bugs rarely come from one missing keyword alone; they emerge from inconsistent ownership, hidden shared mutability, poor contention management, and incorrect assumptions about progress. Production-grade concurrency therefore requires both JVM-level understanding and system-design discipline.
Core Concepts
Thread safety means an object behaves correctly under concurrent use without requiring callers to add extra synchronization around every operation. There are several ways to achieve it: immutability, thread confinement, safe publication, synchronization, and lock-free or wait-free algorithms. Immutability is usually the cheapest in mental overhead because once state cannot change, readers need coordination only for publication, not for every access.
Safe publication is a central concept. Constructing an object safely is not enough; other threads must also observe it through a valid publication edge such as final-field semantics, static initialization, volatile write, lock release, concurrent collection insertion, or task handoff through an executor. Without safe publication, another thread may see stale references or partially initialized state even if the constructor itself looked fine.
Concurrent collections encode specific concurrency strategies. ConcurrentHashMap uses striped/bin-level coordination and non-blocking reads for most operations, making it a strong default for shared maps. CopyOnWriteArrayList favors read-mostly workloads by copying the array on each write. BlockingQueue expresses producer-consumer handoff with built-in blocking semantics. ConcurrentLinkedQueue gives non-blocking FIFO behavior but no backpressure. Choosing among them is really choosing contention, consistency, and memory-allocation trade-offs.
Progress guarantees matter as much as correctness. Deadlock means threads wait forever in a cycle. Livelock means threads are active but make no useful progress, often because they keep backing off in sync. Starvation means one thread or class of work rarely gets CPU or a needed resource because others dominate access. These are not theoretical edge cases; they are common incident patterns in overloaded services.
Practical Usage
Prefer designs that minimize shared mutable state before reaching for clever synchronization. Stateless services, immutable value objects, actor-like ownership, sharded state, and queue-based handoff reduce the number of places where multiple threads can interfere. If a component can be made thread-confined, do that first; it is usually simpler than proving a lock protocol correct.
When shared state is required, pick concurrent collections for the access pattern, not because they sound modern. ConcurrentHashMap is a strong general-purpose choice, but compound actions still need thought. A putIfAbsent or computeIfAbsent can be atomic, whereas containsKey() followed by put() is not. CopyOnWriteArrayList is excellent for listener registries or infrequently modified configuration snapshots, but disastrous for high-write workloads because every mutation copies the full backing array.
Use atomic classes with care. AtomicInteger or AtomicReference is perfect for a small state machine or lock-free flag. Under heavy contention, LongAdder often scales better for counters because it spreads updates across cells. But atomics do not automatically solve compound invariants involving multiple variables. If consistency spans several fields, you still need a stronger coordination strategy.
Code Examples
class MetricsRegistry {
private final ConcurrentHashMap<String, LongAdder> counters = new ConcurrentHashMap<>();
void increment(String name) {
counters.computeIfAbsent(name, key -> new LongAdder()).increment();
}
long current(String name) {
LongAdder adder = counters.get(name);
return adder == null ? 0L : adder.sum();
}
}
This combines two advanced ideas: ConcurrentHashMap for safe shared access and LongAdder for scalable increments under contention. A plain AtomicLong is simpler, but may become a hot CAS bottleneck under very high write rates.
class WorkPipeline {
private final BlockingQueue<Job> queue = new ArrayBlockingQueue<>(1000);
void submit(Job job) throws InterruptedException {
queue.put(job);
}
void workerLoop() {
while (!Thread.currentThread().isInterrupted()) {
try {
Job job = queue.take();
process(job);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
private void process(Job job) {}
}
A BlockingQueue is often a better architecture than a hand-rolled lock protocol because it encodes backpressure, handoff, and waiting semantics directly.
boolean transfer(Account from, Account to, long amount, Lock first, Lock second) throws InterruptedException {
if (!first.tryLock(50, TimeUnit.MILLISECONDS)) {
return false;
}
try {
if (!second.tryLock(50, TimeUnit.MILLISECONDS)) {
return false;
}
try {
from.debit(amount);
to.credit(amount);
return true;
} finally {
second.unlock();
}
} finally {
first.unlock();
}
}
Timed acquisition does not magically prevent all deadlocks, but it converts some “wait forever” failures into visible retry/fallback paths that can be monitored.
Trade-offs
Immutability reduces synchronization complexity drastically, but can increase allocation and copying cost. CopyOnWriteArrayList is the clearest example: fantastic for read-mostly snapshots, terrible for frequent writes. ConcurrentHashMap scales well, but some operations provide weakly consistent iteration rather than a fully frozen view. That is often the right trade, but only if the caller understands it.
Lock-free structures reduce blocking and can improve throughput, yet they shift complexity into CAS loops, retry behavior, and memory-ordering reasoning. Under contention, a non-blocking algorithm may burn CPU instead of parking. Similarly, deadlock avoidance with tryLock and retries can turn into livelock if all contenders behave identically and keep colliding.
The broader trade-off is between local simplicity and system-level predictability. A clever low-contention optimization may look good in isolation but behave badly once mixed with real scheduler behavior, downstream backpressure, or uneven tenant load. Advanced concurrency design optimizes for stable behavior under failure, not only peak benchmark numbers.
Common Mistakes
One major mistake is assuming a concurrent collection makes every compound operation safe. ConcurrentHashMap protects its own internal state, but “check then act” sequences still need atomic APIs or external coordination. Another recurring bug is unsafe publication: storing a freshly constructed mutable object into a plain field and expecting all threads to see it fully initialized.
Teams also overuse CopyOnWriteArrayList because it feels thread-safe and convenient. It is safe, but if the list changes often, each write copies the entire array and creates heavy allocation pressure. With atomics, a common misconception is that replacing a synchronized block with several atomic variables preserves the same invariant. It usually does not unless the invariant is intentionally decomposed.
Deadlock bugs often come from inconsistent lock ordering across code paths. Livelock appears when polite retry logic keeps yielding to competing threads forever. Starvation can come from unfair locks, CPU-heavy tasks monopolizing a shared pool, or readers dominating writers in poorly chosen lock configurations. These are design bugs, not random bad luck.
Senior-level Insights
The best concurrency optimization is often eliminating the need for concurrency control in the first place. Partition by key, isolate state per actor, snapshot immutable configuration, and pass ownership through queues. Every time two threads can mutate the same thing, you have created a long-term reasoning burden that future maintainers must pay.
On the JVM, diagnosis matters as much as design. Use jstack to confirm deadlock cycles, JFR to inspect lock contention and park events, and application metrics to correlate queue depth, timeout rate, and throughput collapse. A deadlock that happens once a week under load is still a design failure; the fact that it is intermittent only makes it more expensive.
Modern Java adds virtual threads, which change the economics of blocking but not the semantics of shared state. Thread safety, safe publication, deadlock, and starvation still exist. In fact, virtual threads make it easier to create huge amounts of concurrency, which means hidden shared-state bottlenecks show up faster. Understanding classic concurrency deeply is what prevents Loom-era systems from failing in more scalable ways.
Glossary
- Thread safety: Correct behavior under concurrent access.
- Safe publication: Making an object visible to other threads through a valid ordering edge.
- ConcurrentHashMap: Scalable concurrent map with weakly consistent iteration.
- CopyOnWriteArrayList: List optimized for read-mostly workloads by copying on writes.
- BlockingQueue: Queue with built-in blocking handoff and backpressure semantics.
- LongAdder: Counter optimized for high-contention increments.
- Deadlock: Cyclic waiting with no progress.
- Livelock: Continuous activity without useful progress.
- Starvation: Work is continually delayed because others dominate access.
Cheatsheet
- Prefer immutability, confinement, and ownership boundaries over more locks.
- Safe construction is not enough; safe publication matters too.
- Use
ConcurrentHashMapatomic methods for compound map updates. - Use
CopyOnWriteArrayListonly when writes are rare. - Use
BlockingQueuewhen you need handoff and backpressure. - Consider
LongAdderfor hot counters under high contention. - Atomics do not protect multi-variable invariants by themselves.
- Prevent deadlock with lock ordering, timeouts, or reduced shared state.
- Watch for livelock in retry-heavy “polite” algorithms.
- Diagnose concurrency failures with thread dumps, JFR, and saturation metrics.
🎮 Games
10 questions