Synchronization.
We have seen what goes wrong with shared mutable state. Now we learn how to prevent it. Synchronization is the art of coordinating access to shared data. Today we understand the fundamental tools: locks, volatile, and atomic operations.
The core idea is mutual exclusion. At most one thread accesses the protected code at a time.
If two threads want to enter the critical section, one waits. The other executes. When done, the waiting thread can proceed.
This eliminates race conditions in the protected section. Only one thread touches the data at once. No interleaving.
Mutual exclusion is the foundation. Locks implement it. Synchronized blocks use it.
The cost is waiting. Threads that could run in parallel must wait for each other. Concurrency is reduced to protect correctness.
A lock is an object that enforces mutual exclusion.
lock dot lock acquires the lock. If another thread holds it, wait. If free, take it.
Inside, you access shared data safely. Only you have the lock.
lock dot unlock releases the lock. Another waiting thread can proceed.
Always unlock. If you forget, other threads wait forever. Use try-finally to ensure unlock even if exceptions occur.
In Kotlin and Java, you can use ReentrantLock or synchronized blocks.
A synchronized block is a simpler way to lock.
synchronized parenthesis object close parenthesis brace code close brace. The object is the lock. Only one thread can be in synchronized blocks on the same object.
Every object in Kotlin and Java can be a lock. You choose what to synchronize on.
Synchronized blocks automatically unlock when exiting. Even on exception. No try-finally needed.
In Kotlin, @Synchronized on a method synchronizes on this for instance methods.
Synchronized blocks are convenient but lock the entire block. Fine-grained locking requires explicit locks.
Synchronization provides three guarantees.
Mutual exclusion: only one thread in the protected section.
Visibility: changes made by one thread are visible to the next thread that acquires the lock. No stale data.
Ordering: operations before releasing the lock happen-before operations after acquiring it. No harmful reordering.
These three together ensure correct behavior. The lock is not just about one-at-a-time. It is about seeing and ordering.
Without the lock, you have none of these guarantees.
Deadlocks occur when threads wait for each other in a cycle.
Thread A holds lock 1, wants lock 2. Thread B holds lock 2, wants lock 1. Neither can proceed. The program freezes.
Deadlocks happen when acquiring multiple locks in different orders. A then B versus B then A.
Preventing deadlocks: always acquire locks in the same order. If everyone takes lock 1 before lock 2, no cycle forms.
Detecting deadlocks: the JVM can detect them. Thread dumps show which threads wait for what.
Deadlocks do not crash. They freeze. The symptom is a program that stops doing anything. No errors, no progress.
Sometimes you do not need mutual exclusion. You just need visibility.
volatile marks a variable for special treatment. Reads and writes go to main memory, not caches.
A write to a volatile variable is immediately visible to all threads. No stale data.
volatile also prevents reordering around the variable.
volatile is enough for flags: var done by volatile equals false. One thread writes, another reads. No race because there is no compound operation.
volatile is not enough for counter plus plus. That is read-modify-write, not atomic. Use atomic types instead.
Atomic operations complete in one indivisible step.
AtomicInteger provides atomic increment: counter dot incrementAndGet.
AtomicReference provides atomic object updates.
Atomics use special CPU instructions. No locking needed. Very fast.
Compare-and-swap: change the value only if it equals the expected value. If another thread changed it first, retry.
Atomics are great for counters, flags, and simple state. For complex state with multiple variables, locks are often clearer.
For simple flags and visibility: volatile.
For counters and simple state: atomic types.
For protecting complex operations on shared data: synchronized or locks.
Use the simplest tool that works. Volatile if visibility is enough. Atomics if you need atomic updates. Locks when you need mutual exclusion over multiple operations.
More complexity means more chances for bugs. Keep synchronization as simple as the problem requires.
The best solution is often avoiding shared mutable state entirely. Immutable data needs no synchronization.
Let us summarize.
Mutual exclusion: one thread at a time in critical sections.
Locks and synchronized: enforce mutual exclusion, provide visibility and ordering.
Deadlocks: circular waiting for locks. Prevent by consistent ordering.
Volatile: visibility without mutual exclusion. For simple flags.
Atomic types: indivisible operations. For counters and simple state.
Choose the right tool. Keep it simple. Prefer avoiding shared mutable state.
Synchronization makes concurrent code correct but requires careful design.