Skip to main content

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:

  1. 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
  1. Keep synchronized sections small

Don't hold a lock longer than necessary.

  1. Use tryLock() with timeout

With ReentrantLock, a thread can try to acquire a lock and stop waiting indefinitely.

  1. 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:

  1. Randomized backoff
  2. Retry limits
  3. Better locking strategy
  4. 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 while loop (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​

  1. Try-Lock: lock.tryLock(timeout, unit) avoids indefinite blocking.
  2. Fairness: new ReentrantLock(true) grants locks to the longest-waiting thread.
  3. Interruptible: lock.lockInterruptibly() responds immediately to Thread.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.