Concurrency Principles
Race conditions, deadlock, volatile, atomics, concurrent collections, and the Java Memory Model.
Deutsche Übersetzung in Arbeit
Diese Lektion ist noch nicht ins Deutsche übersetzt und wird daher auf Englisch angezeigt. Der Rest der Seite ist vollständig lokalisiert.
Auf dieser Seite
Threads sharing memory is where concurrency gets dangerous. Two threads touching the same data at the same time can corrupt it in ways that are maddening to debug. This lesson covers the classic hazards - and how to write safe concurrent code.
The race condition
A race condition happens when the result depends on the unpredictable timing of threads. The classic example: two threads incrementing the same counter.
class Counter {
int count = 0;
void increment() { count++; } // NOT atomic!
}count++ looks like one step, but it's actually three: read count, add
one, write it back. If two threads interleave those steps, updates get lost:
Two editors open the same shared doc showing "10 sales". Both read 10, both add 1, both save 11. Two sales happened, but the count only went up by one - one edit was silently lost. That's a race condition: correct-looking steps, wrong result, because of timing.
Fix 1 - synchronized
The synchronized keyword ensures only one thread runs a block at a time, by
acquiring a lock:
class Counter {
private int count = 0;
synchronized void increment() { count++; } // one thread at a time
synchronized int get() { return count; }
}A one-key bathroom guarantees only one person is inside at a time; others wait for the key. A lock does the same for a block of code - only the thread holding the lock may enter, so shared data can't be corrupted mid-update.
Fix 2 - atomic classes
For simple counters, java.util.concurrent.atomic offers lock-free, thread-safe
types:
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // atomic - safe without explicit locksFix 3 - concurrent collections
Regular collections aren't thread-safe. Use purpose-built concurrent versions:
Map<String, Integer> safe = new ConcurrentHashMap<>();
Queue<Task> queue = new ConcurrentLinkedQueue<>();Best of all: avoid sharing mutable state
The safest concurrent code shares as little mutable data as possible. Prefer immutable objects (which can be shared freely), pass data via thread-safe queues, and confine mutable state to a single thread. Less shared state means fewer ways to go wrong.
Deadlock
A deadlock occurs when threads wait on each other forever. Thread A holds lock 1 and wants lock 2; thread B holds lock 2 and wants lock 1. Neither can proceed.
Five philosophers sit around a table, each needing two forks to eat. If every philosopher grabs the fork on their left and waits for the one on their right - nobody ever eats. Everyone holds something the next person needs. That's deadlock.
Avoid it by always acquiring locks in the same order, holding locks briefly, and using timeouts.
The Java Memory Model and volatile
Threads may cache variables locally, so one thread's change might not be visible
to another. The volatile keyword guarantees that reads and writes go
straight to main memory, so all threads see the latest value:
private volatile boolean running = true; // changes are visible across threads
void stop() { running = false; } // another thread will see thisvolatile ensures visibility, not atomicity
volatile guarantees other threads see the latest value, but it does not
make compound operations like count++ atomic. For counters, use synchronized
or an AtomicInteger. Use volatile for simple flags.
Quick check
Two threads both run count++ on a shared, unsynchronized counter. What can go wrong?
Key takeaways
- A race condition arises when correctness depends on thread timing (e.g. unsynchronized count++).
- synchronized locks a block so only one thread runs it at a time, protecting shared data.
- Atomic classes (AtomicInteger) and concurrent collections (ConcurrentHashMap) are thread-safe by design.
- Deadlock is threads waiting on each other forever; avoid it by acquiring locks in a consistent order.
- volatile guarantees visibility of changes across threads but not atomicity of compound operations.
- Safest of all: minimize shared mutable state and prefer immutable objects.
Now we shift gears from concurrency to a different superpower: treating code as data with lambdas and functional interfaces.