Skip to main content

Chapter 13: Concurrency

Why Concurrency Is Hardโ€‹

Concurrency is one of the most difficult areas of software engineering. It introduces a class of bugs that are timing-dependent, non-deterministic, and extremely hard to reproduce. This chapter explains the fundamental challenges and gives principles for writing cleaner concurrent code.


Why Concurrency?โ€‹

Concurrency is about decoupling what gets done from when it gets done. There are two main motivations:

1. Performanceโ€‹

  • A single-threaded web server handles one request at a time. Concurrency lets it handle many.
  • Long I/O operations (database calls, network requests, file reads) block a thread. Other threads can do useful work while one is blocked.

2. Structureโ€‹

  • Some problems are naturally modeled as multiple independent entities (e.g., a chat server where each user connection is its own entity).

Myths and Misconceptionsโ€‹

Martin lists several common misconceptions:

MythReality
Concurrency always improves performanceOnly if there's meaningful wait time to overlap, or multiple processors available
Design doesn't change with threadsConcurrent design is fundamentally different from single-threaded design
It's not a big deal if the container handles threadsYou must understand what your container does โ€” Spring/Tomcat threads can cause subtle bugs

Concurrency Defense Principlesโ€‹

Single Responsibility Principle for Threadsโ€‹

Concurrent code is complex enough on its own. Keep concurrency code separate from other code.

// Bad โ€” business logic mixed with thread management
public class OrderProcessor implements Runnable {
private final List<Order> orders;

public void run() {
for (Order order : orders) {
// business logic entangled with threading
synchronized (this) {
process(order); // is this thread-safe?
}
}
}
}

// Better โ€” separate concerns
public class OrderProcessor {
public void process(Order order) { /* pure business logic */ }
}

public class OrderProcessorThread implements Runnable {
private final OrderProcessor processor;
// threading concerns live here
}

Limit the Scope of Shared Dataโ€‹

The more places that access shared data, the more likely a race condition. Minimize the number of critical sections and the amount of data they share.

// Bad โ€” shared mutable state accessed from many places
public class Counter {
public int count = 0; // public mutable state โ€” dangerous
}

// Better โ€” encapsulate and control access
public class Counter {
private int count = 0;

public synchronized void increment() { count++; }
public synchronized int getCount() { return count; }
}

// Best in Java โ€” use atomic types
import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
private final AtomicInteger count = new AtomicInteger(0);

public void increment() { count.incrementAndGet(); }
public int getCount() { return count.get(); }
}

Use Copies of Dataโ€‹

If you can copy data rather than sharing it, you eliminate the need for synchronization entirely:

// Each thread works on its own copy โ€” no shared state, no locking needed
List<Order> myOrders = new ArrayList<>(sharedOrders); // defensive copy
for (Order order : myOrders) {
process(order);
}

Threads Should Be as Independent as Possibleโ€‹

Write threads that operate on their own local data and don't share state with other threads. Stateless objects are inherently thread-safe.

// Stateless service โ€” thread-safe by design
@Service
public class TaxCalculator {
public BigDecimal calculate(Order order) {
// uses only the method argument โ€” no shared state
return order.getSubtotal().multiply(TAX_RATE);
}
}

Know Your Libraryโ€‹

Java 5+ introduced java.util.concurrent with powerful tools. Use them:

ToolUse Case
ReentrantLockMore flexible than synchronized
SemaphoreCount-based locking
CountDownLatchWait for multiple threads to complete
ConcurrentHashMapThread-safe map, better than Collections.synchronizedMap
AtomicInteger, AtomicLongLock-free atomic operations
ExecutorServiceThread pool management
CopyOnWriteArrayListThread-safe list for read-heavy workloads
// Don't manage threads manually
Thread thread = new Thread(() -> process(task));
thread.start();

// Use ExecutorService
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> process(task));
executor.shutdown();

Know Your Execution Modelsโ€‹

Producer-Consumerโ€‹

One or more producer threads add to a queue; one or more consumer threads take from it. The queue is the boundary:

BlockingQueue<Order> queue = new LinkedBlockingQueue<>(100);

// Producer
queue.put(newOrder); // blocks if queue is full

// Consumer
Order order = queue.take(); // blocks if queue is empty
process(order);

Readers-Writersโ€‹

Multiple readers can access shared data simultaneously; writers need exclusive access:

ReadWriteLock lock = new ReentrantReadWriteLock();

// Reader
lock.readLock().lock();
try { return data.get(key); }
finally { lock.readLock().unlock(); }

// Writer
lock.writeLock().lock();
try { data.put(key, value); }
finally { lock.writeLock().unlock(); }

Dining Philosophersโ€‹

A classic illustration of deadlock: multiple threads competing for multiple shared resources, each waiting for a resource held by another. Always acquire locks in a consistent order to prevent deadlock.


Beware Dependencies Between Synchronized Methodsโ€‹

Multiple synchronized methods on the same shared class can create subtle bugs:

// Each method is synchronized individually โ€” but together they have a race condition
if (!stack.isEmpty()) // thread A checks
stack.pop(); // thread B pops between the check and this line!

// Better โ€” synchronize the client code
synchronized (stack) {
if (!stack.isEmpty())
stack.pop();
}

Prefer server-side locking: the class itself should be responsible for thread safety, not the callers.


Testing Concurrent Codeโ€‹

Concurrent bugs are notoriously hard to test because they're timing-dependent.

Strategies:

  1. Write tests that expose potential failures โ€” run them many times
  2. Make thread-based code pluggable โ€” run with 1 thread, then many
  3. Run on different platforms โ€” thread scheduling varies by OS and JVM
  4. Instrument code to force failures โ€” use Thread.yield() or Thread.sleep() at critical points to trigger race conditions during testing
  5. Use stress testing tools โ€” tools like Java PathFinder (JPF) can explore thread interleavings

Key Takeawaysโ€‹

  • Concurrency is hard โ€” treat it with respect
  • Separate concurrent code from business logic (SRP)
  • Minimize shared mutable state โ€” immutable and stateless objects are thread-safe by definition
  • Use copies of data to avoid sharing altogether
  • Know and use java.util.concurrent instead of rolling your own
  • Understand the common patterns: Producer-Consumer, Readers-Writers, Dining Philosophers
  • Avoid dependencies between synchronized methods on a shared object
  • Testing concurrent code requires deliberate effort โ€” run tests many times, on multiple platforms