Concurrency
java.util.concurrent (JSR-166) provides high-level building blocks that make concurrent code correct and maintainable without hand-rolling threads and locks. Instead of managing Thread objects directly, you submit tasks to executors, compose asynchronous results, and reach for purpose-built thread-safe data structures.
ExecutorService and Thread Pools
An ExecutorService decouples task submission from execution. You submit work; a pool of reusable threads runs it. This avoids the cost of creating a thread per task and bounds resource usage.
import java.util.concurrent.*;
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> System.out.println("task on " + Thread.currentThread().getName()));
pool.shutdown(); // stop accepting tasks; finish queued ones
pool.awaitTermination(5, TimeUnit.SECONDS);
| Factory | Pool behavior |
|---|---|
newFixedThreadPool(n) | Fixed n threads, unbounded queue |
newCachedThreadPool() | Grows on demand, reuses idle threads |
newSingleThreadExecutor() | One thread, serial execution |
newVirtualThreadPerTaskExecutor() | One virtual thread per task (Java 21) |
Tip: For production, prefer constructing
ThreadPoolExecutordirectly with a bounded queue and an explicit rejection policy.newFixedThreadPool’s unbounded queue can mask overload and exhaust memory.
Callable and Future
Runnable returns nothing. Callable<T> returns a value and may throw checked exceptions. Submitting a Callable yields a Future<T> — a handle to a result that may not exist yet.
import java.util.concurrent.*;
ExecutorService pool = Executors.newSingleThreadExecutor();
Future<Integer> future = pool.submit(() -> {
Thread.sleep(50);
return 6 * 7;
});
Integer result = future.get(); // blocks until ready
System.out.println(result);
pool.shutdown();
Output:
42
Warning:
future.get()blocks the calling thread indefinitely. Use the timed overloadget(timeout, unit)to avoid hanging forever.
CompletableFuture
CompletableFuture enables non-blocking composition of asynchronous pipelines — chain transformations, combine results, and handle errors without ever calling get().
import java.util.concurrent.CompletableFuture;
CompletableFuture<String> pipeline =
CompletableFuture.supplyAsync(() -> "alan")
.thenApply(String::toUpperCase) // transform
.thenApply(s -> "Hello, " + s) // chain
.exceptionally(ex -> "fallback"); // recover
System.out.println(pipeline.join());
Output:
Hello, ALAN
Use thenCompose to flatten nested futures and thenCombine to merge two independent async results.
Concurrent Collections
The synchronized wrappers (Collections.synchronizedMap) lock the whole structure. Concurrent collections use finer-grained strategies for far better throughput under contention.
| Collection | Use case |
|---|---|
ConcurrentHashMap | High-concurrency map; lock-striped, atomic compute/merge |
CopyOnWriteArrayList | Many reads, rare writes (e.g. listener lists) |
ConcurrentLinkedQueue | Lock-free unbounded queue |
BlockingQueue (LinkedBlockingQueue) | Producer/consumer hand-off |
import java.util.concurrent.ConcurrentHashMap;
ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
counts.merge("hits", 1, Integer::sum); // atomic increment, thread-safe
ReentrantLock vs synchronized
synchronized is simple and automatically released. ReentrantLock is an explicit lock offering features synchronized cannot: timed and interruptible acquisition, fairness, and tryLock.
import java.util.concurrent.locks.ReentrantLock;
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// critical section
} finally {
lock.unlock(); // MUST be in finally
}
| Feature | synchronized | ReentrantLock |
|---|---|---|
| Acquire/release | Automatic | Manual (lock/unlock) |
| Try with timeout | No | tryLock(time, unit) |
| Interruptible | No | lockInterruptibly() |
| Fairness option | No | Yes |
| Condition objects | One (wait/notify) | Multiple (newCondition) |
Warning: With
ReentrantLock, alwaysunlock()in afinallyblock. Forgetting to unlock leaks the lock and can deadlock the whole application.
Atomic Classes
For single-variable updates, atomic classes (AtomicInteger, AtomicLong, AtomicReference) provide lock-free thread safety via CPU compare-and-swap (CAS) — faster than locking under contention.
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet(); // atomic ++
counter.updateAndGet(v -> v * 2); // atomic transform
System.out.println(counter.get());
Output:
2
Best Practices
- Submit tasks to executors instead of creating raw threads; always
shutdown(). - Bound your thread pools and queues, with an explicit rejection policy.
- Compose async work with
CompletableFuturerather than blocking onFuture.get(). - Reach for atomic classes for single counters; locks only for multi-variable invariants.
- Always
unlock()infinally; acquire multiple locks in a consistent global order to avoid deadlock. - Prefer concurrent collections over synchronized wrappers for shared, contended data.
Interview Questions
Q: What is the difference between Runnable and Callable?
Runnable.run() returns void and cannot throw checked exceptions. Callable.call() returns a value and may throw checked exceptions. Submitting a Callable returns a Future for the eventual result.
Q: When would you use ReentrantLock over synchronized?
When you need timed or interruptible lock acquisition, tryLock, fairness, or multiple condition variables. For simple mutual exclusion, synchronized is preferable because it cannot leak an unreleased lock.
Q: Why are atomic classes faster than synchronization for counters? They use hardware compare-and-swap (CAS) instructions to update a value without acquiring a lock, avoiding blocking and context switches. Under moderate contention this is significantly faster than monitor-based locking.
Q: How does ConcurrentHashMap differ from Collections.synchronizedMap?
synchronizedMap guards the entire map with a single lock, serializing all access. ConcurrentHashMap uses fine-grained, lock-free reads and per-bin locking on writes, allowing high concurrent throughput, plus atomic operations like compute and merge.