IntermediateReading time: ~11 min

Synchronization

Synchronized, locks, ReentrantLock and ReadWriteLock

Synchronization in Java is about making shared mutable state safe under concurrent access. Strong interview answers explain not only mutual exclusion, but also visibility, happens-before relationships, condition waiting, and when higher-level locking tools are preferable to intrinsic monitors.

1. Definition

What is synchronization?

Synchronization is the set of techniques that keeps concurrent access correct.

That means more than “only one thread enters here”.

It also means:

  • visibility of writes
  • atomicity of compound actions
  • preservation of invariants
  • safe waiting and notification
  • predictable ordering under the Java Memory Model

Why this matters

Without synchronization, threads may observe:

  • stale values
  • half-finished updates
  • broken invariants across multiple fields
  • corrupted shared state

A synchronized program is one in which readers observe a state that could have come from a valid execution order.

What should a strong answer include?

A strong answer mentions:

  • synchronized
  • monitors
  • happens-before
  • wait() / notifyAll()
  • reentrancy
  • ReentrantLock
  • ReadWriteLock
  • visibility versus atomicity

2. Core Concepts

2.1 Intrinsic locking with `synchronized`

The synchronized keyword provides intrinsic locking.

Entering a synchronized block acquires a monitor.

Leaving it releases that monitor, even if an exception occurs.

This gives:

  • mutual exclusion
  • a memory visibility effect

A successful monitor release happens-before a later successful acquisition of the same monitor.

That is why synchronized solves both atomicity and visibility for the guarded state.

2.1.1 Keywords and contracts you should state explicitly here

These are the load-bearing concepts for this topic:

  • synchronized — intrinsic locking keyword
  • monitor — lock associated with an object
  • intrinsic lock — built-in monitor-based lock
  • reentrant lock behavior — same thread can reacquire the same lock
  • happens-before — visibility and ordering relationship in the Java Memory Model
  • wait() — release monitor and suspend until condition may have changed
  • notify() — wake one waiter
  • notifyAll() — wake all waiters
  • spurious wakeup — legal wakeup without the desired condition becoming true
  • ReentrantLock — explicit lock API with more control than intrinsic locking
  • Condition — condition queue associated with an explicit lock
  • ReadWriteLock — lock split into reader and writer coordination

If you can name these precisely, your explanation becomes much clearer.

2.2 Reentrancy and lock scope

Intrinsic locks are reentrant.

The same thread can enter the same monitor multiple times without deadlocking itself.

Every object can act as a monitor.

But not every object should.

Using this, boxed values, or string literals as public lock objects is risky because unrelated code may synchronize on them accidentally.

A private final lock object is usually safer.

2.3 `wait`, `notify`, and `notifyAll`

A thread must own the monitor before calling these methods.

wait() atomically:

  • releases the monitor
  • suspends the thread

The thread may resume because of:

  • notification
  • interruption
  • timeout
  • spurious wakeup

That is why correct code waits in a loop.

Never assume one wakeup means the condition is definitely true.

2.4 Explicit locks

ReentrantLock gives capabilities intrinsic locking does not:

  • tryLock()
  • timed acquisition
  • interruptible acquisition
  • optional fairness
  • multiple Condition queues

ReadWriteLock allows concurrent readers with exclusive writers.

It helps only when the access pattern truly justifies it.

3. Practical Usage

When `synchronized` is the best tool

Use synchronized when:

  • the lock scope is small and local
  • the invariant is easy to describe
  • readability matters more than extra lock features
  • you need a monitor-based condition queue

It is often the cleanest option for protecting a small object graph.

When explicit locks are justified

Use ReentrantLock when you need:

  • timeout-based deadlock avoidance
  • interruptible blocking
  • multiple wait conditions
  • fairness configuration

The benefit is flexibility.

The cost is discipline.

If you forget unlock() in finally, you create a correctness bug.

`ReadWriteLock` realism

ReadWriteLock sounds scalable.

Sometimes it is.

But it only helps when:

  • reads are frequent
  • reads are long enough to amortize coordination cost
  • writes are less frequent
  • contention is real

On tiny critical sections or write-heavy workloads, it may be slower than a plain mutex.

4. Code Examples

Example 1: Simple intrinsic lock

class Counter {
    private final Object lock = new Object();
    private int value;

    int incrementAndGet() {
        synchronized (lock) {
            value++;
            return value;
        }
    }

    int get() {
        synchronized (lock) {
            return value;
        }
    }
}

Key points:

  • both read and write are guarded
  • one lock protects one invariant scope
  • visibility comes from the same monitor protocol

Example 2: Correct wait loop

class BoundedBuffer<T> {
    private final Queue<T> queue = new ArrayDeque<>();
    private final int capacity;

    BoundedBuffer(int capacity) {
        this.capacity = capacity;
    }

    public synchronized void put(T item) throws InterruptedException {
        while (queue.size() == capacity) {
            wait();
        }
        queue.add(item);
        notifyAll();
    }

    public synchronized T take() throws InterruptedException {
        while (queue.isEmpty()) {
            wait();
        }
        T item = queue.remove();
        notifyAll();
        return item;
    }
}

Key points:

  • while, not if
  • wait() is condition-based waiting
  • notifyAll() is safer when multiple wait conditions share a monitor

Example 3: Explicit lock with `tryLock`

class SafeTransfer {
    private final ReentrantLock lock = new ReentrantLock();
    private int balance;

    boolean tryWithdraw(int amount) throws InterruptedException {
        if (!lock.tryLock(100, TimeUnit.MILLISECONDS)) {
            return false;
        }
        try {
            if (balance < amount) {
                return false;
            }
            balance -= amount;
            return true;
        } finally {
            lock.unlock();
        }
    }
}

Key points:

  • tryLock supports timeout-based coordination
  • unlock() must live in finally

5. Trade-offs

Choice Advantage Cost or risk
synchronized Simple, readable, automatic release Fewer control features
wait/notifyAll Built-in condition coordination Easy to misuse without loops
ReentrantLock Timed and interruptible acquisition, multiple conditions Manual unlock discipline
ReadWriteLock Can help read-heavy contention Often overused or slower in practice

Practical trade-off analysis

Synchronization is not one tool.

It is a ladder of control.

Higher control usually means:

  • more expressive power
  • more complexity
  • more room for subtle mistakes

That is why the default should usually be the simplest correct tool.

6. Common Mistakes

Mistake 1: Believing synchronization is only about one thread at a time

It is also about visibility.

Correct approach:

  • explain mutual exclusion and happens-before together

Mistake 2: Waiting with `if` instead of `while`

Spurious wakeups and racing consumers break that logic.

Correct approach:

  • always re-check the condition in a loop

Mistake 3: Locking on publicly accessible objects

This can create accidental contention with unrelated code.

Correct approach:

  • use a private final lock object

Mistake 4: Forgetting `unlock()` in explicit lock code

This can deadlock the component.

Correct approach:

  • always unlock in finally

Mistake 5: Assuming `ReadWriteLock` is automatically faster

It depends on workload shape.

Correct approach:

  • measure real contention before choosing it

Mistake 6: Mixing unrelated conditions on one monitor carelessly

This causes unnecessary wakeups and confusing behavior.

Correct approach:

  • if conditions become complex, consider explicit Condition queues

7. Deep Dive

7.1 Visibility versus atomicity

A shared field problem is not always only one of atomicity.

Sometimes every write is “simple”, but readers still see stale state.

That is why synchronization must be discussed with memory visibility.

7.2 Condition waiting is protocol design

wait() and notifyAll() are not just API calls.

They form a protocol:

  • what condition is guarded?
  • which lock protects it?
  • who changes it?
  • who waits on it?
  • who signals after state transition?

If that protocol is vague, concurrency bugs follow.

7.3 `ReentrantLock` versus intrinsic monitor

Intrinsic locking is often enough.

ReentrantLock becomes justified when the lock policy itself is part of the design.

Examples:

  • interruption-aware waiting
  • timed acquisition
  • several condition queues
  • controlled fairness

7.4 Read-heavy patterns

Sometimes the best answer is not ReadWriteLock.

Sometimes it is:

  • immutable snapshot replacement
  • ConcurrentHashMap
  • reducing shared state entirely

Senior answers compare those alternatives.

8. Interview Questions

1. What does `synchronized` guarantee?

Mutual exclusion and a visibility relationship through monitor release and acquisition.

2. Why is `wait()` used inside a loop?

Because spurious wakeups are legal and the condition must be rechecked.

3. What is reentrancy?

The same thread can reacquire the same lock it already holds.

4. Why is locking on `this` sometimes risky?

Because external code may synchronize on the same object.

5. What does `notifyAll()` do?

It wakes all waiting threads on the monitor.

6. When is `ReentrantLock` better than `synchronized`?

When timed, interruptible, or multi-condition locking behavior is needed.

7. Is `ReadWriteLock` always faster?

No.

Its benefit depends on workload.

8. What is a happens-before edge in this context?

A visibility and ordering relationship created by synchronization operations.

9. Why is `unlock()` in `finally` mandatory?

Because lock release must happen even on exceptions.

10. What is the senior-level takeaway?

Synchronization is as much protocol design as it is keyword usage.

9. Glossary

Term Meaning
synchronized Intrinsic locking construct
monitor Lock associated with an object
happens-before Visibility and ordering relationship
reentrant Same thread can reacquire the same lock
wait() Release monitor and suspend
notifyAll() Wake all waiters on a monitor
spurious wakeup Wakeup without desired condition becoming true
ReentrantLock Explicit lock with more control
Condition Condition queue tied to an explicit lock
ReadWriteLock Reader/writer coordination lock

10. Cheatsheet

  • synchronized gives mutual exclusion and visibility
  • Use one clear lock per invariant scope
  • Wait in while, not if
  • wait() releases the monitor while waiting
  • notifyAll() is often safer than notify()
  • ReentrantLock adds timed and interruptible acquisition
  • Put unlock() in finally
  • ReadWriteLock helps only on the right workload
  • Private lock objects are safer than public lock targets
  • InterjĂșban nevezd meg a happens-before, reentrancy Ă©s spurious wakeup fogalmakat

🎼 Games

10 questions