Chapter 13: Concurrency
clean-code concurrency threading parallel-programming
Status: Notes complete
Difficulty: Hard
Time to complete: ~50 min read
Overview
Concurrency is one of the most deceptively difficult topics in software engineering. Code that looks correct can behave incorrectly under concurrent execution in ways that are nearly impossible to reproduce reliably. Robert Martin frames concurrency not just as a performance technique, but as a decoupling strategy: it separates what gets done from when it gets done.
This chapter does not teach you everything about concurrency — entire books exist for that. Instead, it gives you the principles and heuristics to write concurrent code that does not silently corrupt data, deadlock under load, or produce results you cannot explain.
Cross-references: ch09-unit-tests, ch10-classes, ch11-systems, ch07-error-handling
The Problem: What Bad Code Looks Like
// BAD — single-threaded web handler; blocks the entire server
public class WebServer {
public void handleRequests() {
while (true) {
Socket connection = serverSocket.accept(); // blocks here
processRequest(connection); // then blocks here — no other requests served
}
}
}While processRequest() is running (maybe doing a slow database query or an external API call), every other incoming connection just waits. On a busy server, this is catastrophic. The fix is not merely “add a thread” — it requires understanding what you’re sharing between threads and what invariants you need to protect.
Core Principles
1. Why Concurrency? Decoupling What from When
Concurrency lets the system do useful work while waiting. Two distinct motivations:
Throughput: A web server handling 1,000 concurrent requests needs threads. Without concurrency, each request blocks the next one. With a thread pool, requests overlap their I/O wait time.
Responsiveness: A UI application downloads a file in the background while remaining interactive. The download logic runs on a worker thread; the UI thread is never blocked.
Data processing pipelines: Stage 1 reads files, Stage 2 parses them, Stage 3 writes results. Pipeline stages run concurrently — while Stage 3 writes record N, Stage 2 is parsing record N+1, and Stage 1 is reading record N+2.
// GOOD — concurrent web server with thread pool
public class WebServer {
private final ExecutorService threadPool = Executors.newFixedThreadPool(50);
public void handleRequests() {
while (true) {
final Socket connection = serverSocket.accept();
threadPool.submit(() -> processRequest(connection)); // non-blocking dispatch
}
}
}// C++ equivalent — thread pool dispatch
class WebServer {
boost::asio::thread_pool pool{50};
public:
void handleRequests() {
while (true) {
auto conn = acceptConnection();
boost::asio::post(pool, [conn]() { processRequest(conn); });
}
}
};# Python equivalent — ThreadPoolExecutor
from concurrent.futures import ThreadPoolExecutor
import socket
class WebServer:
def __init__(self):
self.executor = ThreadPoolExecutor(max_workers=50)
def handle_requests(self):
while True:
conn, addr = self.server_socket.accept()
self.executor.submit(self.process_request, conn)2. Myths and Misconceptions
These are common beliefs that lead engineers to write dangerous concurrent code.
Myth A: “Concurrency always improves performance”
Concurrency only helps when there is wait time to overlap. If your code is CPU-bound and does no I/O, adding threads on a 4-core machine beyond 4 threads will hurt performance (context switching overhead). If your code does lots of I/O (database calls, network), threads let you overlap that wait — and throughput improves dramatically.
// BAD assumption — parallelizing pure CPU work with too many threads
// On a 4-core machine, 100 threads computing Fibonacci does not go 25x faster
// It goes slower due to context switching
ExecutorService pool = Executors.newFixedThreadPool(100);
for (int i = 0; i < 10000; i++) {
pool.submit(() -> computeFibonacci(40)); // CPU-only, no I/O
}
// GOOD — match thread count to available cores for CPU-bound work
int cores = Runtime.getRuntime().availableProcessors();
ExecutorService pool = Executors.newFixedThreadPool(cores);Myth B: “Design does not change with concurrency”
The entire design changes. You must think about: which data is shared, which invariants span multiple operations, who owns what. A class that was perfectly fine in single-threaded code can be dangerously wrong in multi-threaded code without a single line change.
// BAD — looks correct, but getBalance/deduct are not atomic together
public class BankAccount {
private int balance = 1000;
public int getBalance() { return balance; }
public void deduct(int amount) { balance -= amount; }
}
// Thread 1 and Thread 2 both call: if (account.getBalance() >= 500) account.deduct(500)
// Both threads read balance = 1000
// Both pass the check
// Both deduct 500 → balance ends at 0 instead of 500
// RACE CONDITION — balance went negativeMyth C: “Containers like web servers handle it, so I don’t have to think about it”
False. The container manages the thread lifecycle, but it does not know what shared state your code accesses. If two threads both call UserService.getCachedUser() and that cache is a HashMap (not a ConcurrentHashMap), you have a race condition — and the container cannot protect you.
3. Concurrency Defense Principles
3a. Single Responsibility Principle for Concurrency
Concurrency code has its own lifecycle, its own failure modes, and its own testing challenges. It is a separate concern. Do not mix business logic with synchronization code inside the same class.
// BAD — business logic mixed with synchronization
public class OrderProcessor {
private final List<Order> pendingOrders = new ArrayList<>();
public synchronized void addOrder(Order o) {
pendingOrders.add(o);
// ... 50 lines of business logic while holding the lock
sendConfirmationEmail(o); // slow I/O while synchronized!
}
}
// GOOD — separate the concurrency concern
public class OrderQueue {
private final BlockingQueue<Order> queue = new LinkedBlockingQueue<>();
public void enqueue(Order o) { queue.put(o); }
public Order dequeue() throws InterruptedException { return queue.take(); }
}
public class OrderProcessor {
private final OrderQueue orderQueue;
public void process(Order o) {
// Pure business logic, no synchronization
validateOrder(o);
chargePayment(o);
sendConfirmationEmail(o);
}
}3b. Limit the Scope of Shared Data
The more places in the code that touch shared mutable data, the more places a race condition can hide. Encapsulate shared data and provide the narrowest possible interface to it. Keep synchronized blocks small — only the lines that actually need synchronization.
// BAD — synchronized on the entire method while doing slow work
public synchronized void processUserActivity(String userId) {
User user = userMap.get(userId); // needs lock
String report = generateReport(user); // slow, doesn't need lock — but we're holding it!
emailReport(report); // very slow, doesn't need lock — still holding it!
userMap.put(userId, user); // needs lock
}
// GOOD — minimize synchronized scope
public void processUserActivity(String userId) {
User user;
synchronized (userMap) {
user = userMap.get(userId); // grab data quickly
} // release lock immediately
String report = generateReport(user); // slow work outside lock
emailReport(report); // slow I/O outside lock
synchronized (userMap) {
userMap.put(userId, user); // write back quickly
}
}// C++ — std::lock_guard for scoped locking
class UserRegistry {
std::unordered_map<std::string, User> userMap;
std::mutex mtx;
public:
User getUser(const std::string& id) {
std::lock_guard<std::mutex> lock(mtx); // released at end of scope
return userMap.at(id);
}
void updateUser(const std::string& id, const User& u) {
std::lock_guard<std::mutex> lock(mtx);
userMap[id] = u;
}
};# Python — threading.Lock for critical section
import threading
class UserRegistry:
def __init__(self):
self._users = {}
self._lock = threading.Lock()
def get_user(self, user_id: str):
with self._lock: # released automatically
return self._users.get(user_id)
def update_user(self, user_id: str, user):
with self._lock:
self._users[user_id] = user3c. Use Copies of Data
If a shared object is read frequently but rarely written, consider handing each thread its own copy. This eliminates synchronization entirely for the reading path.
// GOOD — avoid sharing by copying
public class ReportGenerator {
// Instead of passing the shared userList (with synchronization),
// take a snapshot at the start of the report generation
public Report generate(List<User> sharedUserList) {
List<User> snapshot;
synchronized (sharedUserList) {
snapshot = new ArrayList<>(sharedUserList); // defensive copy
}
// Work on the snapshot — no locks needed for the rest of the method
return buildReport(snapshot);
}
}3d. Threads Should Be as Independent as Possible
The ideal thread is one that runs in complete isolation — it reads from its own input, writes to its own output, and never touches shared state at all. Structure work to partition data so that independent subsets are processed by independent threads.
// GOOD — partition data; each thread owns its shard, no shared state
public void processAllUsers(List<User> users) {
int numThreads = Runtime.getRuntime().availableProcessors();
List<List<User>> partitions = partition(users, numThreads);
List<Future<?>> futures = new ArrayList<>();
for (List<User> partition : partitions) {
futures.add(executor.submit(() -> {
for (User u : partition) {
processUser(u); // each thread processes its own slice
}
}));
}
// Wait for all to complete
for (Future<?> f : futures) f.get();
}4. Know Your Library
Using the right concurrency primitives prevents you from re-inventing synchronization incorrectly.
Java (java.util.concurrent)
| Class/Interface | Use Case |
|---|---|
ConcurrentHashMap | Thread-safe map without full lock contention |
CopyOnWriteArrayList | Frequently read, rarely written list |
BlockingQueue | Producer-consumer handoff |
ExecutorService | Thread pool management |
Future / CompletableFuture | Async computation result handling |
ReentrantLock | More flexible than synchronized (tryLock, timed lock) |
ReentrantReadWriteLock | Many readers, few writers — concurrent reads |
AtomicInteger / AtomicReference | Lock-free single-variable updates |
CountDownLatch | Wait for N events before proceeding |
Semaphore | Limit concurrent access to a resource pool |
// GOOD — use ConcurrentHashMap instead of synchronized HashMap
// BAD
Map<String, Integer> counts = Collections.synchronizedMap(new HashMap<>());
// Entire map locked on each operation; get+put not atomic together
// GOOD
ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
counts.compute("key", (k, v) -> v == null ? 1 : v + 1); // atomic updateC++ (<thread>, <mutex>, <atomic>, <condition_variable>)
#include <thread>
#include <mutex>
#include <atomic>
#include <condition_variable>
// std::atomic for lock-free counter
std::atomic<int> requestCount{0};
requestCount.fetch_add(1, std::memory_order_relaxed);
// std::condition_variable for signaling between threads
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
// Producer signals:
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one();
// Consumer waits:
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return ready; }); // spurious wakeup safePython (threading, concurrent.futures, asyncio)
import threading
import asyncio
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
# threading.Lock — mutual exclusion
lock = threading.Lock()
with lock:
shared_counter += 1
# concurrent.futures — high-level thread/process pools
with ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(fetch_url, urls)) # I/O-bound
with ProcessPoolExecutor(max_workers=4) as executor:
results = list(executor.map(compute_hash, data_chunks)) # CPU-bound
# asyncio — single-threaded cooperative concurrency for I/O
async def fetch_all(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
return await asyncio.gather(*tasks)Python’s GIL (Global Interpreter Lock) means threads do not run Python bytecode in parallel. Use
ThreadPoolExecutorfor I/O-bound work (threads release GIL during I/O), andProcessPoolExecutorfor CPU-bound work (separate processes bypass the GIL).
5. Know Your Execution Models
Three classic concurrent execution patterns appear in almost every system. Knowing them saves you from re-discovering their failure modes.
5a. Producer-Consumer
One or more producer threads create items and place them in a bounded buffer. One or more consumer threads take items and process them. Producers must block when the buffer is full; consumers must block when it is empty. The buffer is the shared resource.
// GOOD — Java BlockingQueue as the bounded buffer
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class JobQueue {
private final BlockingQueue<Job> queue = new LinkedBlockingQueue<>(100); // capacity 100
// Producer
public void submitJob(Job job) throws InterruptedException {
queue.put(job); // blocks if queue is full — no busy-waiting
}
// Consumer
public Job takeJob() throws InterruptedException {
return queue.take(); // blocks if queue is empty
}
}// C++ — std::queue + condition variables
#include <queue>
#include <mutex>
#include <condition_variable>
class BoundedBuffer {
std::queue<int> buffer;
const size_t maxSize;
std::mutex mtx;
std::condition_variable notFull, notEmpty;
public:
explicit BoundedBuffer(size_t max) : maxSize(max) {}
void produce(int item) {
std::unique_lock<std::mutex> lock(mtx);
notFull.wait(lock, [this] { return buffer.size() < maxSize; });
buffer.push(item);
notEmpty.notify_one();
}
int consume() {
std::unique_lock<std::mutex> lock(mtx);
notEmpty.wait(lock, [this] { return !buffer.empty(); });
int item = buffer.front();
buffer.pop();
notFull.notify_one();
return item;
}
};# Python — queue.Queue handles all synchronization internally
import queue
import threading
job_queue = queue.Queue(maxsize=100)
def producer():
for job in generate_jobs():
job_queue.put(job) # blocks if full
job_queue.put(None) # sentinel to signal done
def consumer():
while True:
job = job_queue.get() # blocks if empty
if job is None:
break
process(job)
job_queue.task_done()
threading.Thread(target=producer).start()
threading.Thread(target=consumer).start()5b. Readers-Writers
A shared resource is read frequently but written infrequently. Multiple readers can safely read simultaneously, but a writer needs exclusive access. The failure mode is writer starvation: if readers constantly hold the lock, writers can never get in.
// GOOD — ReentrantReadWriteLock
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class SharedConfig {
private Map<String, String> config = new HashMap<>();
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public String get(String key) {
rwLock.readLock().lock(); // multiple threads can hold read lock simultaneously
try {
return config.get(key);
} finally {
rwLock.readLock().unlock();
}
}
public void set(String key, String value) {
rwLock.writeLock().lock(); // exclusive — all readers and writers blocked
try {
config.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
}// C++ — std::shared_mutex (C++17)
#include <shared_mutex>
#include <unordered_map>
class SharedConfig {
std::unordered_map<std::string, std::string> config;
mutable std::shared_mutex rwMutex;
public:
std::string get(const std::string& key) const {
std::shared_lock<std::shared_mutex> lock(rwMutex); // shared (read) lock
return config.at(key);
}
void set(const std::string& key, const std::string& value) {
std::unique_lock<std::shared_mutex> lock(rwMutex); // exclusive (write) lock
config[key] = value;
}
};# Python — threading.RLock (or use a read-write lock from rwlock package)
import threading
class SharedConfig:
def __init__(self):
self._config = {}
self._lock = threading.RLock() # re-entrant lock; use rwlock for true read-write
def get(self, key: str) -> str:
with self._lock:
return self._config.get(key)
def set(self, key: str, value: str):
with self._lock:
self._config[key] = valuePython’s
threadingmodule does not have a built-in read-write lock. For true concurrent reads, usethreading.Event+ a counting semaphore, or installreaderwriterlockfrom PyPI.
5c. Dining Philosophers (Deadlock and Livelock)
Five philosophers sit at a round table. Five forks lie between them. Each philosopher needs both the left and right fork to eat. If every philosopher picks up their left fork simultaneously, all are waiting for a right fork that no one will release: deadlock.
// BAD — symmetric pickup causes deadlock
public class Philosopher implements Runnable {
private final Object leftFork, rightFork;
public void run() {
while (true) {
synchronized (leftFork) { // philosopher 0 picks left fork
synchronized (rightFork) { // philosopher 0 waits for right fork
eat(); // deadlock: all waiting for right forks
}
}
}
}
}
// GOOD — break symmetry: one philosopher picks right fork first
public class Philosopher implements Runnable {
private final Object firstFork, secondFork;
public Philosopher(int id, Object[] forks) {
if (id == forks.length - 1) { // last philosopher picks in reverse order
firstFork = forks[0];
secondFork = forks[id];
} else {
firstFork = forks[id];
secondFork = forks[id + 1];
}
}
public void run() {
while (true) {
synchronized (firstFork) {
synchronized (secondFork) {
eat();
}
}
}
}
}Livelock is related: philosophers each pick up a fork, detect the other is taken, put theirs back, and retry — all in perfect synchrony. They’re not blocked (deadlock) but they never make progress either.
The general principle: deadlock requires four conditions (Coffman conditions):
- Mutual exclusion — resources cannot be shared
- Hold and wait — holding one resource while waiting for another
- No preemption — resources cannot be taken forcibly
- Circular wait — a circular chain of waiting exists
Break any one condition to prevent deadlock. Breaking circular wait by imposing an ordering on lock acquisition (like the philosopher solution above) is the most practical approach.
6. Beware Dependencies Between Synchronized Methods
If a class has more than one synchronized method, callers who invoke two of those methods together may be doing something that is not atomic at the class level — even though each method is individually synchronized.
// BAD — two synchronized methods; caller cannot make them atomic together
public class UserAccount {
private int balance;
private String lastTransactionId;
public synchronized int getBalance() { return balance; }
public synchronized void setBalance(int b) { balance = b; }
public synchronized String getLastTransactionId() { return lastTransactionId; }
// Caller code (BAD):
// Thread 1 and Thread 2 both execute:
// if (account.getBalance() >= 100) { // Thread 1 reads 150, Thread 2 reads 150
// account.setBalance(account.getBalance() - 100); // RACE: both deduct, balance goes to 50 instead of 50
// }
// Each method is synchronized; the combination is not.
}
// GOOD — provide a single synchronized operation that does the compound action
public class UserAccount {
private int balance;
public synchronized boolean deductIfSufficient(int amount) {
if (balance >= amount) {
balance -= amount;
return true;
}
return false;
}
}The rule: if clients must call multiple synchronized methods together, move the compound operation into the class itself. Client-side locking (where callers lock on the object) is fragile and spreads the synchronization concern outside the class.
7. Keep Synchronized Sections Small
Every lock acquisition has cost: thread scheduling, memory barrier overhead, and contention when threads compete. Keep what’s inside a lock to the absolute minimum.
// BAD — entire method synchronized, including slow I/O
public class AuditLogger {
private final List<String> log = new ArrayList<>();
public synchronized void logEvent(String userId, String action) {
String entry = buildLogEntry(userId, action); // string formatting — doesn't need lock
String enriched = fetchUserDetails(userId); // HTTP call — DEFINITELY doesn't need lock
log.add(enriched + ": " + entry); // only this line needs the lock
}
}
// GOOD — build the entry outside the lock, only protect the mutation
public class AuditLogger {
private final List<String> log = new ArrayList<>();
public void logEvent(String userId, String action) {
String entry = buildLogEntry(userId, action);
String enriched = fetchUserDetails(userId); // slow I/O outside lock
synchronized (log) {
log.add(enriched + ": " + entry); // only the mutation is locked
}
}
}8. Writing Correct Shut-Down Code Is Hard
Graceful shutdown under concurrent execution is a separate, difficult problem. Threads waiting on a BlockingQueue.take() call will never wake up if the queue is empty and no one puts a new item in. Interrupt handling must be designed in from the start.
// BAD — worker threads deadlock on shutdown because they're blocked in queue.take()
public class WorkerPool {
private final BlockingQueue<Task> queue = new LinkedBlockingQueue<>();
private final List<Thread> workers = new ArrayList<>();
public void shutdown() {
for (Thread t : workers) {
t.interrupt(); // interrupt() wakes threads blocked in sleep/wait/take
}
// Problem: if a thread is not in an interruptible call, interrupt() is ignored
}
}
// GOOD — poison pill pattern: send a sentinel value that tells threads to exit
public class WorkerPool {
private static final Task POISON_PILL = new Task("SHUTDOWN");
private final BlockingQueue<Task> queue = new LinkedBlockingQueue<>();
private final List<Thread> workers;
private class Worker implements Runnable {
public void run() {
try {
while (true) {
Task task = queue.take();
if (task == POISON_PILL) break; // clean exit
processTask(task);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore interrupt flag
}
}
}
public void shutdown() {
for (int i = 0; i < workers.size(); i++) {
queue.put(POISON_PILL); // one pill per worker
}
}
}Key rules for shutdown:
- Think about shutdown in the design phase, not as an afterthought
- Use the interrupt mechanism correctly — always restore the interrupt flag after catching
InterruptedException - Use poison pills for
BlockingQueue-based designs - Use
ExecutorService.shutdown()+awaitTermination()when using the Executor framework
9. Testing Threaded Code
Concurrent bugs are time-dependent, platform-dependent, and load-dependent. They may occur once in a million executions. Standard unit tests that pass once give false confidence.
Guidelines for testing concurrent code:
-
Treat spurious failures as threading issues — if a test fails once and then passes, do not dismiss it as a fluke. Investigate.
-
Get the non-threaded code working first — test the business logic in isolation before introducing threads. A bug in the non-threaded logic will be nearly impossible to diagnose under concurrency.
-
Make the threaded code pluggable — design the code so it can run with 1 thread, 2 threads, or 1,000 threads without changing the business logic.
// GOOD — pluggable thread count for testing
public class TaskProcessor {
private final int threadCount;
private final ExecutorService executor;
public TaskProcessor(int threadCount) {
this.threadCount = threadCount;
this.executor = Executors.newFixedThreadPool(threadCount);
}
// In tests: new TaskProcessor(1) for predictability, new TaskProcessor(100) for stress
}-
Run with more threads than processors — with more threads than cores, the OS must context-switch more frequently, exposing race conditions that don’t appear when each thread runs on its own core uninterrupted.
-
Run on different platforms — Windows and Linux have different thread scheduling behaviors. A race condition visible on one platform may be hidden on another.
-
Instrument code to force failures (jiggling) — insert
Thread.sleep(1),Thread.yield(), orThread.currentThread().interrupt()at strategic points to change the scheduling order and expose races.
// GOOD — instrument to force a context switch at a critical point (test/debug only)
public void transfer(Account from, Account to, int amount) {
synchronized (from) {
Thread.yield(); // force context switch HERE during testing
synchronized (to) {
from.deduct(amount);
to.credit(amount);
}
}
}Remove instrumentation before deploying to production.
Comparison / Summary Table — Concurrent Execution Models
| Model | Pattern | Shared Resource | Key Problem | Java Solution | C++ Solution | Python Solution |
|---|---|---|---|---|---|---|
| Producer-Consumer | Bounded buffer between stages | The buffer (queue) | Blocking on full/empty | BlockingQueue | std::queue + condition_variable | queue.Queue |
| Readers-Writers | Many read, few write | The shared data | Writer starvation | ReentrantReadWriteLock | std::shared_mutex | threading.RLock (partial) |
| Dining Philosophers | N resources, N+1 contenders | Shared resource pool | Deadlock / livelock | Break symmetry in lock ordering | Break symmetry in lock ordering | Break symmetry in lock ordering |
When to Apply / Common Exceptions
Apply concurrent design when:
- I/O wait time is significant (database calls, HTTP requests, file I/O)
- You need to keep a UI or service responsive while doing background work
- You have embarrassingly parallel data-processing workloads
Be cautious when:
- The code is purely CPU-bound — more threads than cores hurts on CPUs without hyperthreading
- The shared state is complex — sometimes a simpler single-threaded design with an event queue is cleaner and safer
- You’re in Python doing CPU-bound work — the GIL means threads don’t help; use
ProcessPoolExecutororasyncio
Prefer these over raw threads when possible:
ExecutorService/CompletableFuture(Java) over manually managingThreadobjectsconcurrent.futures(Python) overthreading.Threaddirectlystd::async(C++) over rawstd::threadfor simple cases
Checklist
- Concurrency code is in its own class, separate from business logic (SRP)
- Shared mutable data is minimized; access points are clearly identified
-
synchronized/locksections are as small as possible — no slow I/O inside locks - Thread-safe collections used (
ConcurrentHashMap,BlockingQueue) instead of manually synchronizedHashMap/ArrayList - No two synchronized methods called together from outside the class without an atomic compound operation inside the class
- Shutdown logic designed from the start — interrupt flags restored, poison pills used
- Tests run with 1, 2, and many-threads to expose race conditions
- Spurious test failures treated as threading bugs, not flukes
- Code reviewed by someone who understands the Java Memory Model / C++ memory model
Key Takeaways
- Concurrency decouples what from when — it is a design strategy, not just a performance hack.
- Concurrency is hard — not because of complexity in a single thread, but because of the combinatorial explosion of possible interleavings between threads.
- Keep concurrency code separate — it has its own lifecycle, challenges, and failure modes; don’t mix it with business logic.
- Minimize shared data — every piece of shared mutable state is a potential race condition. Prefer copies, immutable objects, and message passing.
- Use the right library primitives —
BlockingQueue,ConcurrentHashMap,ReentrantReadWriteLockexist precisely to save you from implementing correct synchronization yourself. - Know the three execution models — Producer-Consumer, Readers-Writers, and Dining Philosophers cover the overwhelming majority of concurrent design problems.
- Synchronized sections should be tiny — hold locks for the minimum possible time; never do I/O while holding a lock.
- Shutdown is a first-class concern — design it from day one; it is harder than startup.
- Test with stress — run tests with more threads than processors, on different platforms, and instrument code to force interleavings.
- Spurious failures are bugs — a test that fails intermittently is testing a race condition, not a fluke.
Related Resources
- ch09-unit-tests — Testing principles apply to concurrent tests too; the FIRST principles help structure them
- ch10-classes — SRP for classes applies equally to concurrency: keep synchronization separate
- ch11-systems — Systems-level thinking about construction and concern separation
- ch07-error-handling —
InterruptedExceptionhandling is part of concurrency error handling - ch17-smells-and-heuristics — Several heuristics in Chapter 17 address concurrency smells directly
Last Updated: 2026-04-14