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
ReentrantLockReadWriteLock- 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 changednotify()â wake one waiternotifyAll()â wake all waiters- spurious wakeup â legal wakeup without the desired condition becoming true
ReentrantLockâ explicit lock API with more control than intrinsic lockingConditionâ condition queue associated with an explicit lockReadWriteLockâ 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
Conditionqueues
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, notifwait()is condition-based waitingnotifyAll()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:
tryLocksupports timeout-based coordinationunlock()must live infinally
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
Conditionqueues
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
synchronizedgives mutual exclusion and visibility- Use one clear lock per invariant scope
- Wait in
while, notif wait()releases the monitor while waitingnotifyAll()is often safer thannotify()ReentrantLockadds timed and interruptible acquisition- Put
unlock()infinally ReadWriteLockhelps 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