6.2 Thread Synchronization, Monitors & Explicit Locks
← Back to Chapter 6: Multithreading & Concurrency
When multiple threads concurrently modify shared mutable state, non-atomic read-modify-write sequences cause race conditions and data corruption.
Synchronization
Synchronization controls multiple threads when they are accessing the same shared resource at the same time. Think of a single bank account shared by two threads.
Thread 1 ──────┐
├──→ Shared Bank Account
Thread 2 ──────┘
If both modify the account simultaneously, problems can occur.
Problems that Synchronization solves
1. Race Condition — the biggest problem ✅
A race condition happens when multiple threads access and modify shared data at the same time, and the final result depends on which thread will execute first. Example : Suppose,
Balance = ₹1000
Two threads want to withdraw ₹700 at the same time.
Thread 1 → Withdraw ₹700
Thread 2 → Withdraw ₹700
Both might read:
Balance = ₹1000
Then both think:
1000 >= 700 → Yes
Both withdraw. Final balance could become:
₹1000 - ₹700 - ₹700 = -₹400
But this should never happen.
Synchronization solves this
↓
Locks the resource
↓
Reads balance
↓
Withdraws money
↓
Releases lock
↓
Thread 2 enters
↓
Reads updated balance
So only one thread modifies the balance at a time.
2. Data Inconsistency ✅
Because of race conditions, shared data can become incorrect or inconsistent. For example: Stock = 1
Thread 1 → buys the last product
Thread 2 → buys the last product
//Without synchronization:
Both see Stock = 1
Both purchase
Now you've sold one product twice. With synchronization:
Thread 1 → Stock 1 → Purchase → Stock 0
↓
releases lock
Thread 2 → Stock 0 → Cannot purchase
So synchronization protects the correctness of shared data.
3. Lost Update ✅
This is another common concurrency problem. Suppose: Counter = 10 Two threads execute:
counter++;
It looks like one operation, but internally it is approximately:
1. Read counter
2. Add 1
3. Write counter
Now:
Thread 1 → reads 10
Thread 2 → reads 10
Thread 1 → writes 11
Thread 2 → writes 11
Expected:
12
Actual:
11
One update was lost. Synchronization solves this
Only one thread can perform the complete read → modify → write operation at a time.
4. Visibility Problem ✅
Sometimes one thread changes a variable, but another thread **may not immediately see the updated value **because of CPU caching and the Java Memory Model. Example:
boolean running = true;
Thread 1:
running = false
Thread 2:
while (running) {
// keep working
}
You expect Thread 2 to eventually see false. But without proper memory visibility guarantees, Thread 2 may continue seeing its previously observed value.
Solution
Volatile or Synchronous Locks
volatile is particularly useful when you need visibility, but not a compound atomic operation like count++.
5. Atomicity Problem ✅
Some operations look like one operation but actually consist of multiple steps. For example:
count++;
is not atomic.
Another thread can interfere between:
READ
↓
MODIFY
↓
WRITE
Synchronization makes the entire critical section execute as one protected operation.
6. Deadlock — Synchronization doesn't solve it ❌
Deadlock means: Two or more threads are waiting for each other forever.
Synchronization can actually cause deadlocks if locks are used incorrectly. Imagine:
Thread 1:
Lock A
↓
Waiting for Lock B
Thread 2:
Lock B
↓
Waiting for Lock A
So:
Thread 1 → has A → wants B
↑
│
Thread 2 → has B → wants A
Neither can continue.
How do we prevent deadlock? Common techniques:
- Always acquire locks in the same order
Thread 1 → A → B
Thread 2 → A → B
rather than:
Thread 1 → A → B
Thread 2 → B → A
- Keep synchronized sections small
Don't hold a lock longer than necessary.
- Use tryLock() with timeout
With ReentrantLock, a thread can try to acquire a lock and stop waiting indefinitely.
- Avoid unnecessary nested locks
The more locks a thread holds simultaneously, the greater the deadlock risk.
7. Starvation ❌
Imagine 5 threads are waiting for a resource. But because of unfair scheduling/locking, T1 keeps getting the resource while T2–T5 keep waiting. This is called Starvation.
T1 → gets lock
T1 → releases
T1 → gets lock again
T1 → releases
T1 → gets lock again
Other threads may wait for a very long time.
Solutions - Use fair locking where appropriate:
new ReentrantLock(true);
The true requests a fairness policy so waiting threads are given a more orderly chance to acquire the lock.
Also avoid holding locks for unnecessarily long periods.
8. Livelock ❌
Threads are active, but they keep responding to each other and make no progress. It is not deadlock.
Imagine two people trying to pass through a doorway:
Person A → moves left
Person B → moves left
Person A → moves right
Person B → moves right
Person A → left
Person B → left
They're moving, but nobody gets through.
In software, this can happen when threads repeatedly detect a conflict, back off, retry, and collide again.
Solution - Use techniques such as:
- Randomized backoff
- Retry limits
- Better locking strategy
- Timeouts
Locks
When multiple threads want to access the same data/resource, a lock ensures that only the thread holding the lock can access the protected part at that time.
Simple example:
Shared Bank Account
│
┌────────┴────────┐
↓ ↓
Thread 1 Thread 2
│ │
└────── 🔒 ───────┘
Thread 1 gets the lock → Thread 2 has to wait → Thread 1 finishes → lock is released → Thread 2 can proceed.
What does a lock actually provide?
A lock mainly gives you mutual exclusion.
Mutual Exclusion = only one thread can execute the protected code at a time.
Lock vs synchronized
The idea is the same:
synchronized Lock
│ │
└──── protects ────────┘
↓
Shared Resource
But Lock gives you more control.
Inter-Thread Signaling: wait(), notify(), and notifyAll()
- Can only be called from inside a synchronized block owning the monitor lock.
wait(): Releases the monitor lock and suspends the thread into the object's wait set.- Must always be called inside a
whileloop (guarding against spurious wakeups):
synchronized (lock) {
while (!condition) {
lock.wait(); // Spurious wakeup safe
}
// Proceed with state modification
}
Explicit Locks: java.util.concurrent.locks
ReentrantLock Capabilities
- Try-Lock:
lock.tryLock(timeout, unit)avoids indefinite blocking. - Fairness:
new ReentrantLock(true)grants locks to the longest-waiting thread. - Interruptible:
lock.lockInterruptibly()responds immediately toThread.interrupt().
Lock lock = new ReentrantLock();
lock.lock();
try {
// Critical Section
} finally {
lock.unlock(); // Always release in finally block!
}
ReentrantReadWriteLock
Separates read operations from write operations:
- Multiple concurrent readers can hold the read lock simultaneously.
- Only one writer can hold the write lock (exclusively blocking all readers and other writers).
- Ideal for read-heavy caches.