Last updated: 2026-09-26
The Concurrent Toolbelt
Raw new Thread(...) is concurrency's goto: legal, occasionally necessary, and almost never the right first move. Modern platforms provide a toolbelt of higher abstractions that manage the dangerous parts for you. This page tours the essentials, in the order you should reach for them.
Thread Pools: Stop Creating Threads by Hand
// Don't: a thread per task — unbounded, expensive, unmanageable
new Thread(() -> handle(request)).start();
// Do: a pool sized to the resource, tasks as values
ExecutorService pool = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors());
Future<Receipt> future = pool.submit(() -> checkout(order)); // returns immediately
Receipt receipt = future.get(); // rendezvous when needed
pool.shutdown();
An ExecutorService separates what to run (tasks) from how it runs (worker threads, queueing, lifecycle). Threads are recycled instead of created per task, the pool size caps resource use, and shutting down is one call instead of a manhunt. Rule of thumb: CPU-bound pools sized near the core count; IO-bound work given many more (or, in modern Java, virtual threads via Executors.newVirtualThreadPerTaskExecutor(), which makes thread-per-task cheap again for waiting workloads).pool size is a tuning knob, not free
Atomic Variables: Fixing the Counter Properly
class Counter {
private final AtomicLong count = new AtomicLong();
void increment() { count.incrementAndGet(); } // one indivisible hardware operation
long value() { return count.get(); }
}
The racing counter loses updates because read-add-write can interleave. AtomicLong performs the whole update as a single hardware compare-and-swap instruction — no lock, no lost updates, and under contention it simply retries. The java.util.concurrent.atomic family (and ConcurrentHashMap, LongAdder beside it) covers most "shared counter/flag/cache" needs without a synchronized in sight.no lock, but it can spin under load
Locks, When You Must
private final Object lock = new Object();
private final Map<String, Balance> balances = new HashMap<>();
void transfer(String from, String to, long pence) {
synchronized (lock) { // one guardian for one invariant
balances.get(from).debit(pence);
balances.get(to).credit(pence); // both-or-neither, never observed half-done
}
}
When an invariant spans multiple variables — debit and credit must move together — atomics can't help; you need mutual exclusion. Keep three habits: one named lock per invariant (not "synchronise everything", which serialises your program back to one thread); tiny critical sections (never do I/O while holding a lock); and a global acquisition order when multiple locks are unavoidable, which forecloses the deadlock by construction.
The Quiet Superpower: Immutability
record PriceList(Map<String, Long> pricesPence) {
PriceList { pricesPence = Map.copyOf(pricesPence); } // defensive, immutable copy
}
// Publish a NEW list; readers holding the old one are unaffected, no locks anywhere
volatile PriceList current = load();
void refresh() { current = load(); }
Thread safety is not something you add; it is something you stop needing. An immutable object can be read by any number of threads forever with zero synchronisation, because no observation can ever be half-complete. Design the majority of your objects immutable (records again), confine mutation to a few well-named places guarded by the tools above, and most of your codebase becomes trivially, provably thread-safe.
Choosing From the Belt
| Situation | Reach for |
|---|---|
| Run many independent tasks | ExecutorService (fixed pool CPU-bound; virtual threads IO-bound) |
| Shared counter, flag, or single reference | AtomicLong / AtomicReference / LongAdder |
| Shared map or queue | ConcurrentHashMap, BlockingQueue |
| Invariant spanning several fields | One synchronized block / ReentrantLock, held briefly |
| Data that is read far more than written | Immutable snapshots republished via volatile |
| Cross-thread hand-off of work or results | Queues and messages — not shared structures (see Event-Driven Programming) |
Related Topics
- The Shared Mutability Disaster — the race condition and deadlock this page's tools exist to fix.
- The Illusion of Simultaneity — what a thread actually is and why scheduling is nondeterministic, the background this page's tool choices assume.
- Concurrency Gaps: Lessons from Building a Real Language — what goes wrong when a language's own concurrency toolbelt is incomplete, drawn from real bugs found building one from scratch.
- Event-Driven Programming — the message-passing alternative to shared state referenced in the table above, developed in full.