Advanced Topics in Java Concurrency
As you grow beyond the basics of threads, synchronization, and executors, advanced concurrency concepts become critical to building scalable and resilient systems. This article covers deadlocks, livelocks, starvation, ThreadLocal variables, Future vs CompletableFuture, parallel streams, and best practices for large systems.
1. Deadlocks, Livelocks, and Starvation
Deadlock
Occurs when two or more threads wait indefinitely for each other to release resources.
Example:
class DeadlockDemo {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void method1() {
synchronized (lock1) {
System.out.println("Thread 1 acquired lock1");
synchronized (lock2) {
System.out.println("Thread 1 acquired lock2");
}
}
}
public void method2() {
synchronized (lock2) {
System.out.println("Thread 2 acquired lock2");
synchronized (lock1) {
System.out.println("Thread 2 acquired lock1");
}
}
}
}
If method1()
and method2()
run simultaneously, a deadlock may occur.
Prevention strategies:
- Acquire locks in a consistent global order.
- Use
tryLock()
with timeouts. - Prefer higher-level concurrency abstractions.
Livelock
Threads keep responding to each other but make no progress.
Analogy: Two people trying to pass each other in a hallway, both stepping left/right repeatedly.
Starvation
A thread never gets CPU or resource access because others dominate.
Example: Low-priority threads in a heavily loaded system.
Fix:
- Use fair locks (
new ReentrantLock(true)
). - Ensure balanced scheduling.
2. ThreadLocal
ThreadLocal
provides thread-specific variables. Each thread accessing a ThreadLocal
variable has its own isolated copy.
Example:
public class ThreadLocalDemo {
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
Runnable task = () -> {
int value = threadLocal.get();
threadLocal.set(value + 1);
System.out.println(Thread.currentThread().getName() + " -> " + threadLocal.get());
};
new Thread(task, "T1").start();
new Thread(task, "T2").start();
}
}
Use cases:
- Store user session data.
- Store DB connection or transaction context.
- Avoid passing context through multiple layers.
⚠️ Be careful with ThreadLocal leaks in thread pools (always call remove()
).
3. Future vs CompletableFuture
Future
Represents the result of an async computation.
- Provides
get()
,cancel()
, andisDone()
. - Blocking:
get()
waits until result is ready. - No chaining or composition.
CompletableFuture
Introduced in Java 8, it improves on Future
:
- Non-blocking and chainable (
thenApply
,thenCompose
,thenAccept
). - Can combine multiple async tasks (
allOf
,anyOf
). - Supports timeouts and exceptions.
Example:
CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(s -> s + " World")
.thenAccept(System.out::println);
Output: Hello World
4. Parallel Streams (Java 8+)
Streams API allows parallel processing by splitting tasks across multiple threads using the ForkJoinPool.
Example:
List<Integer> list = IntStream.range(1, 100).boxed().toList();
int sum = list.parallelStream()
.mapToInt(Integer::intValue)
.sum();
System.out.println(sum);
- Parallel streams use the common ForkJoinPool.
- Suitable for CPU-bound tasks.
- ⚠️ Avoid for I/O-bound tasks (blocking will harm throughput).
- ⚠️ Be careful with shared mutable state inside parallel streams.
5. Best Practices for Large Systems
- Avoid shared mutable state — prefer immutability.
- Use higher-level abstractions (
ExecutorService
,CompletableFuture
) over raw threads. - Apply backpressure — prevent unbounded task submission.
- Use monitoring & profiling to detect deadlocks, thread leaks, or contention.
- Separate CPU-bound and I/O-bound tasks with different executors.
- Use timeouts (
Future.get(timeout)
,tryLock(timeout)
) to prevent infinite blocking. - Gracefully handle shutdowns — always call
shutdown()
andawaitTermination()
.
Summary
Advanced concurrency requires understanding not just how to create threads, but also how to avoid pitfalls like deadlocks and starvation, use ThreadLocal wisely, leverage CompletableFuture for async programming, and apply parallel streams correctly. These tools and best practices are crucial for scalable, production-grade systems and are frequently tested in FAANG interviews.