Loading learning content...
In 1985, computer scientist Leslie Lamport wrote: "A distributed system is one in which the failure of a computer you didn't even know existed can render your own computer unusable." The same insight applies to concurrent programming: a thread you didn't even know was running can corrupt data you thought was yours alone.
At the heart of nearly every concurrency bug lies a single culprit: shared mutable state. These three words—shared, mutable, state—represent the perfect storm that enables race conditions, data corruption, and the subtle, intermittent failures that haunt production systems.
Understanding shared mutable state isn't just about identifying a category of bugs—it's about developing a mental model that guides every design decision in concurrent systems.
By the end of this page, you will understand why shared mutable state is the fundamental source of concurrency hazards, how to identify it in your codebase, and the three strategies for eliminating or managing it. You'll develop the instinct to spot dangerous patterns and make informed decisions about when and how to share state between threads.
Let's break down the three components and understand why their combination creates problems.
State: Data stored in memory—variables, fields, data structures. State represents information that a program maintains and uses.
Mutable: State that can be changed after initialization. Any field that can be reassigned, any collection that can be modified, any object whose internal values can be altered.
Shared: State that can be accessed by more than one thread. This includes global/static variables, object fields accessible via shared references, and any data passed between threads.
Here's the key insight: any one of these properties is safe on its own. Problems only arise when all three combine:
| Combination | Safe? | Explanation |
|---|---|---|
| Shared + Mutable + State | ❌ UNSAFE | The dangerous combination—multiple threads can simultaneously read and write |
| Shared + Immutable + State | ✅ Safe | No thread can change state, so reads are always consistent |
| Unshared + Mutable + State | ✅ Safe | Only one thread accesses, so no conflicts possible (thread confinement) |
| Shared + Mutable + No State | ✅ Safe | Pure functions with no side effects—nothing to corrupt |
To achieve thread safety, you must eliminate at least ONE of the three properties: make state immutable, prevent sharing (thread confinement), or eliminate state entirely (stateless operations). Most thread-safe designs apply one or more of these strategies rather than trying to synchronize shared mutable state.
This insight transforms how we approach concurrent design:
Let's explore why the dangerous combination is so problematic.
When multiple threads can read and write the same data, three categories of hazards emerge:
1. Data Races
A data race occurs when two or more threads access the same memory location concurrently, at least one access is a write, and no synchronization orders the accesses. Data races cause undefined behavior in most languages—anything can happen.
2. Race Conditions
A race condition occurs when the correctness of a program depends on the relative timing or interleaving of multiple threads. Unlike data races (which are about unsynchronized memory access), race conditions are about logical errors in the sequence of operations.
3. Memory Consistency Errors
Due to caching, compiler optimizations, and instruction reordering, one thread's writes may not be visible to another thread in a timely or ordered manner, even without data races.
12345678910111213141516171819202122232425262728293031323334353637383940414243
// Conceptual example of shared mutable state hazards// (Actual JavaScript is single-threaded in main context, but Worker threads can share memory) class BankAccount { private balance: number; constructor(initialBalance: number) { this.balance = initialBalance; } // HAZARD 1: Data Race // Two threads reading/writing 'balance' without synchronization getBalance(): number { return this.balance; // Read may see stale/partial value } // HAZARD 2: Race Condition (check-then-act) // Even if getBalance() and setBalance() were atomic individually... withdraw(amount: number): boolean { if (this.balance >= amount) { // Thread A: balance=100, amount=60 → true // Thread B: balance=100, amount=60 → true // ...context switch here... this.balance -= amount; // Thread A: balance = 40 // Thread B: balance = -20 (INVARIANT VIOLATED!) return true; } return false; } // HAZARD 3: Compound operation non-atomicity transfer(other: BankAccount, amount: number): boolean { if (this.withdraw(amount)) { // Step 1: Deduct from this // System crashes here → money is lost! other.deposit(amount); // Step 2: Add to other return true; } return false; } deposit(amount: number): void { this.balance += amount; // Read-modify-write: NOT atomic! }}The BankAccount example illustrates all three hazards:
Data Race: Multiple threads reading and writing balance without synchronization. On some platforms, reading a double during a write can yield a corrupted value (torn read).
Race Condition: The withdraw method has a check-then-act pattern. Between the check (balance >= amount) and the action (balance -= amount), another thread can modify the balance, invalidating the check.
Compound Non-Atomicity: The transfer method involves multiple steps that must all succeed together, but there's no guarantee of atomicity across the operations.
Let's trace through exactly how a shared mutable state bug manifests, step by step. This detailed analysis builds intuition for recognizing these patterns in real code.
Scenario: A concurrent user session counter in a web application.
12345678910111213141516
public class SessionManager { // Shared mutable state private int activeSessionCount = 0; public void onSessionStart() { activeSessionCount++; // Read (1) → Add (2) → Write (3) } public void onSessionEnd() { activeSessionCount--; } public int getActiveCount() { return activeSessionCount; }}This looks innocent enough. Let's examine what activeSessionCount++ actually does at the instruction level:
1. READ: Load activeSessionCount from memory into CPU register (value: 5)
2. ADD: Add 1 to the register value (value: 6)
3. WRITE: Store register value back to memory (activeSessionCount = 6)
Now consider two threads executing onSessionStart() simultaneously:
| Step | Thread A | Thread B | Register A | Register B | Memory Value |
|---|---|---|---|---|---|
| Initial | — | — | — | — | 5 |
| 1 | READ activeSessionCount | — | 5 | — | 5 |
| 2 | — | READ activeSessionCount | 5 | 5 | 5 |
| 3 | ADD 1 | — | 6 | 5 | 5 |
| 4 | — | ADD 1 | 6 | 6 | 5 |
| 5 | WRITE 6 | — | 6 | 6 | 6 |
| 6 | — | WRITE 6 | 6 | 6 | 6 |
| Final | Complete | Complete | — | — | 6 (should be 7!) |
Both threads read the same initial value (5), both add 1 to get 6, and both write 6. One increment is completely lost. With high concurrency, you might process 10,000 session starts but only count 8,500. Your monitoring dashboards show 15% fewer users than reality.
Why didn't we catch this in testing?
On a developer's laptop, thread scheduling is relatively deterministic. The window between read and write is microseconds. The probability of two threads hitting that exact window is low in light testing.
But in production:
The fix requires atomic operations:
12345678910111213141516171819202122232425262728293031323334353637383940414243
import java.util.concurrent.atomic.AtomicInteger; public class SessionManager { // Thread-safe atomic counter private final AtomicInteger activeSessionCount = new AtomicInteger(0); public void onSessionStart() { activeSessionCount.incrementAndGet(); // Atomic: read+add+write as ONE operation } public void onSessionEnd() { activeSessionCount.decrementAndGet(); } public int getActiveCount() { return activeSessionCount.get(); }} // OR: Using synchronized public class SessionManagerSync { private int activeSessionCount = 0; private final Object lock = new Object(); public void onSessionStart() { synchronized (lock) { activeSessionCount++; // Protected by lock } } public void onSessionEnd() { synchronized (lock) { activeSessionCount--; } } public int getActiveCount() { synchronized (lock) { return activeSessionCount; } }}Not all shared state is equally obvious. Developing thread-safe code requires recognizing shared state in all its forms—including subtle ones that are easy to overlook.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
import java.util.*;import java.util.concurrent.*; public class SubtleSharingExamples { // Example 1: Closure captures private int counter = 0; // Instance field public void runInBackground() { Runnable task = () -> { counter++; // BUG: 'counter' is captured and now shared // with whatever thread runs this Runnable! }; executor.submit(task); } // Example 2: Container contents become shared private final List<User> users = new CopyOnWriteArrayList<>(); // Thread-safe list public void registerUser(User user) { users.add(user); // Now 'user' is accessible from any thread // reading the list. Is User itself thread-safe? } public void updateAllUsers() { for (User user : users) { user.incrementLoginCount(); // BUG: User might not be thread-safe! } } // Example 3: Memoization creates shared state private volatile ExpensiveObject cached; // Lazy singleton pattern public ExpensiveObject getExpensive() { if (cached == null) { cached = new ExpensiveObject(); // BUG: Multiple threads might // create multiple instances! } return cached; // The returned object is now shared } // Example 4: Return value sharing public List<String> getActiveUserNames() { List<String> names = new ArrayList<>(); // ... populate names ... return names; // BUG: If caller stores this reference, // and another thread calls this method, // they might get the same ArrayList instance! } // Example 5: External resource sharing private final FileWriter logFile; // External state public void log(String message) { logFile.write(message); // BUG: FileWriter isn't thread-safe! // Concurrent writes corrupt output. } private final ExecutorService executor = Executors.newFixedThreadPool(4);}If object A is thread-safely published to multiple threads, every object reachable from A is also effectively shared. Thread safety analysis must follow the entire reference graph, not just direct fields. A thread-safe container of non-thread-safe elements is NOT thread-safe as a whole.
Given that shared mutable state is the root problem, we have three fundamental strategies to achieve thread safety. Each removes one component of the dangerous triad.
Remove Mutability: Make state immutable.
Immutable objects are inherently thread-safe because their state cannot change after construction. No synchronization is ever needed when sharing immutable data.
How to achieve immutability:
final (or equivalent)this escape)Example:
12345678910111213141516171819202122
// Immutable class - always thread-safepublic final class ImmutablePoint { private final int x; private final int y; public ImmutablePoint(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } // "Modification" returns NEW object public ImmutablePoint translate(int dx, int dy) { return new ImmutablePoint(x + dx, y + dy); }} // Usage: safely share across threadsImmutablePoint origin = new ImmutablePoint(0, 0);// Can pass 'origin' to any thread with zero synchronizationWhen to use: Whenever possible! Immutability should be the default. The performance cost of creating new objects is often far less than the complexity cost of synchronizing mutable state.
Common immutable types: String, Integer, LocalDate, BigDecimal, most value objects.
Developing the skill to spot shared mutable state requires systematic analysis. Here's a framework for reviewing code for concurrency hazards.
final or is a mutable collection/object.1234567891011121314151617181920212223242526272829303132333435363738394041424344
public class UserService { // REVIEW POINT 1: Is this shared? // - It's an instance field of UserService // - UserService might be a singleton (need to check) // - ConcurrentHashMap is thread-safe for individual operations private final Map<String, User> cache = new ConcurrentHashMap<>(); // ✓ OK // REVIEW POINT 2: Is this shared? // - Instance field, mutable (not final, even if effectively so) // - Multiple threads could access via getUserStats() private long totalLogins = 0; // ❌ UNSAFE: read-modify-write // REVIEW POINT 3: Method parameter analysis public void login(User user) { // Is 'user' thread-safe? user.setLastLoginTime(now()); // ❌ If User is shared, this mutates shared state! cache.put(user.getId(), user); // ❌ After this, 'user' IS shared via cache totalLogins++; // ❌ RACE CONDITION: read-modify-write not atomic } // REVIEW POINT 4: Return value analysis public User getUser(String id) { return cache.get(id); // ❌ Caller receives reference to shared User // Any mutations by caller affect shared state! } // REVIEW POINT 5: Compound operation public User getOrCreate(String id, Supplier<User> factory) { User user = cache.get(id); // ← Thread A reads null if (user == null) { // ← Thread B reads null user = factory.get(); // ← Both create new User cache.put(id, user); // ← Thread A puts its User } // ← Thread B overwrites with its User! return user; // ❌ RACE: factory called twice, wrong user returned } // CORRECT: Use atomic compute operation public User getOrCreateCorrect(String id, Supplier<User> factory) { return cache.computeIfAbsent(id, k -> factory.get()); // ✓ Atomic }}Static analysis tools can help identify potential concurrency issues: Java has FindBugs/SpotBugs, IntelliJ IDEA's inspections, and Error Prone. Go has the built-in race detector (go run -race). C++ has ThreadSanitizer. These tools catch many issues, but they can't replace careful design.
We've explored the root cause of concurrency hazards and the strategies for addressing them. Let's consolidate the key insights:
What's next:
Now that we understand how shared mutable state creates hazards, we'll examine the specific manifestation of those hazards: race conditions. The next page provides a deep dive into the anatomy of race conditions, their various forms, and techniques for detecting and preventing them.
You now understand why shared mutable state is the fundamental source of concurrency bugs, how to identify it in various forms, and the three strategies for achieving thread safety. This knowledge prepares you to recognize and address race conditions in the next page.