Why Synchronize?
Synchronization solves both race conditions and data races by providing two guarantees:
- Mutual exclusion — Only one thread can execute the protected section at a time.
- Memory visibility — Changes made by a thread inside a synchronized block are visible to the next thread that enters the same synchronized block.
The synchronized Keyword
Synchronized Methods
public class BankAccount {
private double balance;
public synchronized void deposit(double amount) {
balance += amount;
}
public synchronized void withdraw(double amount) {
balance -= amount;
}
public synchronized double getBalance() {
return balance;
}
}
When a method is marked synchronized, it acquires the lock on this (the instance) before executing. This means all synchronized methods on the same object share one lock — if Thread A is inside deposit(), Thread B cannot enter withdraw() on the same instance.
For static synchronized methods, the lock is on the Class object:
public static synchronized void globalOperation() {
// Locks on BankAccount.class
}
Synchronized Blocks
Synchronized blocks give finer-grained control by letting you choose which object to lock on:
public class TransferService {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
private double accountA = 1000;
private double accountB = 1000;
public void creditAccountA(double amount) {
synchronized (lock1) {
accountA += amount;
}
}
public void creditAccountB(double amount) {
synchronized (lock2) {
accountB += amount;
}
}
}
Using separate lock objects means operations on accountA and accountB can execute in parallel — they don’t block each other.
Choosing What to Lock On
| Lock Target | Use Case | Risk |
|---|---|---|
this | Simple cases, small classes | Other code can lock on your instance externally |
private final Object | Encapsulated locking | None — recommended for libraries |
ClassName.class | Static synchronization | Coarse, blocks all static synchronized methods |
Always prefer private final Object lock = new Object() for lock objects:
privateprevents external code from locking on itfinalprevents accidental reassignment (which would change the lock identity)
Synchronization Approaches Comparison

Comparison Table
| Approach | Mutual Exclusion | Visibility | Tryable | Timeout | Fair | Interruptible | Condition Support |
|---|---|---|---|---|---|---|---|
volatile | ✗ | ✓ | — | — | — | — | — |
synchronized | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | via wait/notify |
ReentrantLock | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ (Condition) |
ReadWriteLock | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
StampedLock | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ |
| Atomic classes | ✗ (lock-free) | ✓ | — | — | — | — | — |
Pros and Cons of synchronized
Pros
- Simple syntax — built into the language, no import needed.
- Automatic release — the lock is always released when the block exits, even on exceptions.
- JVM optimizations — biased locking, lock coarsening, lock elision by HotSpot.
- No boilerplate — no try/finally needed.
Cons
- No timeout — a thread waiting for a
synchronizedlock waits forever. - Not interruptible — you cannot interrupt a thread waiting to enter a
synchronizedblock. - No fairness — threads can starve; no FIFO guarantee.
- No tryLock — you cannot attempt to acquire and fall back if unavailable.
- Coarse granularity — method-level synchronization locks the entire object.
- No read/write distinction — readers block other readers unnecessarily.
Inter-Thread Communication with wait/notify
Every Java object has a built-in wait set. Threads can wait for a condition and be notified when it changes:
public class BoundedBuffer<T> {
private final Queue<T> queue = new LinkedList<>();
private final int capacity;
public BoundedBuffer(int capacity) {
this.capacity = capacity;
}
public synchronized void put(T item) throws InterruptedException {
while (queue.size() == capacity) {
wait(); // Release lock and wait until notified
}
queue.add(item);
notifyAll(); // Wake up consumers
}
public synchronized T take() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // Release lock and wait until notified
}
T item = queue.poll();
notifyAll(); // Wake up producers
return item;
}
}
Rules of wait/notify
- Must be called from within a
synchronizedblock on the same object. - Always use
wait()inside awhileloop — spurious wakeups can occur. - Prefer
notifyAll()overnotify()unless you have exactly one waiter and one condition. wait()releases the lock atomically and reacquires it when woken up.
Why a while Loop?
// WRONG — if/then
synchronized (lock) {
if (condition) {
lock.wait(); // Might wake up spuriously — condition could still be true
}
// proceed — but condition might not actually hold!
}
// CORRECT — while loop
synchronized (lock) {
while (condition) {
lock.wait(); // Re-check after every wakeup
}
// proceed — condition is guaranteed to be false
}
Deadlocks
A deadlock occurs when two or more threads each hold a lock and wait for a lock held by another, creating a circular dependency.

Four Conditions for Deadlock
All four must be true simultaneously for a deadlock to exist:
- Mutual exclusion — The resource can only be held by one thread.
- Hold and wait — A thread holds one resource while waiting for another.
- No preemption — Resources cannot be forcibly taken from a thread.
- Circular wait — A cycle of threads, each waiting for the next.
Deadlock Example
public class DeadlockDemo {
private final Object lockA = new Object();
private final Object lockB = new Object();
public void method1() {
synchronized (lockA) { // Acquires A
synchronized (lockB) { // Waits for B
// work
}
}
}
public void method2() {
synchronized (lockB) { // Acquires B
synchronized (lockA) { // Waits for A — DEADLOCK!
// work
}
}
}
}
Prevention Strategies
| Strategy | How | Trade-off |
|---|---|---|
| Lock ordering | Always acquire locks in the same global order | Requires discipline; hard to enforce in large codebases |
| Try-lock with timeout | Use ReentrantLock.tryLock(timeout) | Adds complexity; need fallback logic |
| Single lock | Protect everything with one lock | Reduces parallelism |
| Lock-free algorithms | Avoid locks entirely using CAS | Complex to implement correctly |
| Avoid nested locks | Restructure code to acquire only one lock | Not always possible |
Rule of Thumb
Avoid unsorted locking. If you must acquire multiple locks, always acquire them in a consistent, globally defined order (e.g., by object hash code or ID):
public void transfer(Account from, Account to, double amount) {
// Always lock the account with the smaller ID first
Account first = from.getId() < to.getId() ? from : to;
Account second = from.getId() < to.getId() ? to : from;
synchronized (first) {
synchronized (second) {
from.withdraw(amount);
to.deposit(amount);
}
}
}
Summary
synchronizedis the simplest synchronization tool — automatic lock release, JVM-optimized.- Use dedicated
private final Objectlock objects for fine-grained control. wait/notifyenable inter-thread communication but must always be used insidewhileloops.- Deadlocks require all four conditions simultaneously — break any one to prevent them.
- When
synchronizedisn’t flexible enough (timeout, tryLock, fairness), reach forReentrantLock.