Shared state problems.
Threads share memory. This seems useful, but it is the source of the hardest bugs in concurrent programming. Today we understand what goes wrong when threads share mutable state without protection.
When multiple threads access the same data, and at least one writes, problems occur.
Read-only sharing is safe. Ten threads reading the same value see the same thing.
But if one thread writes while another reads or writes, chaos ensues. The reader might see partial writes. Two writers might overwrite each other.
This is not a bug in your logic. Your code might be perfect for sequential execution. But concurrent execution changes everything.
The same code that works sequentially can fail concurrently. This is subtle. This is dangerous.
A race condition is when the result depends on the timing of operations.
Two threads increment a counter. The final value should be 2. But sometimes it is 1. Who wins the race to read, modify, write?
Race conditions are non-deterministic. Run the program ten times, get different results. The bug appears sometimes, not always.
Race conditions are hard to reproduce. The timing that triggers the bug might be rare. Tests pass 999 times, fail once.
Race conditions are hard to debug. By the time you add logging, the timing changes. The bug disappears.
A data race is a specific kind of race condition.
One thread writes a variable while another thread reads or writes it, without synchronization.
Data races cause undefined behavior. The reading thread might see partial writes. It might see old values. It might see corrupted data.
A race condition is about logic depending on timing. A data race is about memory access without coordination.
All data races are race conditions. Not all race conditions are data races.
Preventing data races is the first step to correct concurrent code.
Many operations that look like one step are actually multiple steps.
count plus plus looks atomic. It is not. It reads count, adds one, writes count. Three steps.
If two threads do count plus plus simultaneously, both might read the same value, both add one, both write. You wanted 2, you got 1.
This is called a lost update. One update is lost because the other did not see it.
Even simple operations are not atomic. Assigning a long on some platforms is two writes. The reader might see half of each value.
Never assume an operation is atomic. Check the documentation. Use atomic types when needed.
When one thread writes, do other threads see the write?
CPUs have caches. A thread might write to its cache. Other threads read from their caches. They do not see the update.
This is a visibility problem. The write happened. It is just not visible.
Without proper synchronization, a thread might never see updates from another thread. It might read stale data forever.
A common pattern: a flag to stop a loop. done equals true in one thread. while not done in another. Without visibility guarantees, the loop might never see done become true.
Visibility requires synchronization. Just because you wrote a value does not mean others see it.
Compilers and CPUs reorder operations for performance.
You write: a equals 1 semicolon b equals 2. The CPU might execute: b equals 2 semicolon a equals 1. For a single thread, this is fine.
But another thread might see b equals 2 before a equals 1. It expects if b is 2, a is 1. But a is still 0. Logic breaks.
Reordering is invisible in single-threaded code. It becomes visible in multi-threaded code.
Synchronization prevents harmful reordering. Without it, you cannot assume operations happen in the order you wrote them.
Unsynchronized concurrent access is unpredictable.
It might work perfectly on your machine. It might fail on another machine with different timing.
It might work under light load. It might fail under heavy load when timing changes.
It might work with one JVM. It might fail with another that optimizes differently.
You cannot test your way to correctness. A passing test does not prove absence of race conditions.
You must reason about all possible interleavings. You must design for correctness, not just hope.
Signs of shared state problems:
Inconsistent results. The same operation gives different outcomes.
Corruption. Data structures end up in impossible states.
Deadlocks. The program freezes. Threads wait for each other forever.
Rare failures. Bugs that happen occasionally, impossible to reproduce.
If you see these symptoms in concurrent code, suspect shared state problems. Check every variable that multiple threads access.
Let us summarize.
Shared mutable state plus concurrent access equals problems.
Race conditions: results depend on timing.
Data races: unsynchronized reads and writes.
Non-atomic operations: compound steps can interleave.
Visibility: writes may not be seen by other threads.
Reordering: operations may not execute in written order.
These problems are subtle, unpredictable, and hard to debug.
Next, we learn how to prevent them.