Complete guide to Synchronization in Java — race conditions, synchronized methods and blocks, intrinsic locks, volatile keyword, atomic variables, and deadlock prevention.
Why Synchronization is Needed
When multiple threads access shared mutable data simultaneously, race conditions occur — leading to corrupted data:
public class RaceConditionDemo {
private static int counter = 0;
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Race Condition Demo ===\n");
Runnable increment = () -> {
for (int i = 0; i < 100_000; i++) {
counter++; // NOT atomic! Read → Modify → Write
}
};
Thread t1 = new Thread(increment, "T1");
Thread t2 = new Thread(increment, "T2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Expected: 200,000");
System.out.println("Actual: " + counter);
System.out.println("Lost updates: " + (200_000 - counter));
System.out.println("\n⚠️ counter++ is NOT atomic!");
System.out.println("It's actually 3 steps:");
System.out.println(" 1. READ counter from memory");
System.out.println(" 2. ADD 1 to the value");
System.out.println(" 3. WRITE result back to memory");
System.out.println("Threads can interleave between these steps!");
}
}
Output (varies each run):
| Expected | 200,000 |
| Actual | 163,847 |
| Lost updates | 36,153 |
The synchronized Keyword
Synchronized Method
public class SynchronizedMethod {
private int counter = 0;
// Only ONE thread can execute this at a time (per instance)
public synchronized void increment() {
counter++; // Now thread-safe!
}
public synchronized int getCounter() {
return counter;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedMethod obj = new SynchronizedMethod();
Runnable task = () -> {
for (int i = 0; i < 100_000; i++) {
obj.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("=== Synchronized Method ===");
System.out.println("Expected: 200,000");
System.out.println("Actual: " + obj.getCounter());
System.out.println("✓ Always correct with synchronization!");
}
}
Output:
=== Synchronized Method ===
Expected: 200,000
Actual: 200,000
✓ Always correct with synchronization!
Synchronized Block (Preferred — More Fine-Grained)
public class SynchronizedBlock {
private int balance = 10000;
private final Object lock = new Object(); // Lock object
public void withdraw(int amount) {
// Non-synchronized code (runs in parallel)
System.out.println(Thread.currentThread().getName() + " wants to withdraw " + amount);
// Only THIS block is synchronized
synchronized (lock) {
if (balance >= amount) {
// Simulate processing delay
try { Thread.sleep(10); } catch (InterruptedException e) {}
balance -= amount;
System.out.println(" " + Thread.currentThread().getName() +
" withdrew " + amount + ". Balance: " + balance);
} else {
System.out.println(" " + Thread.currentThread().getName() +
" DENIED. Insufficient funds. Balance: " + balance);
}
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedBlock account = new SynchronizedBlock();
System.out.println("=== Synchronized Block (Bank Account) ===");
System.out.println("Starting balance: 10000\n");
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(() -> account.withdraw(3000), "Customer-" + (i + 1));
threads[i].start();
}
for (Thread t : threads) t.join();
System.out.println("\nFinal balance: " + account.balance);
}
}
Output:
| Starting balance | 10000 |
| Customer-1 withdrew 3000. Balance | 7000 |
| Customer-2 withdrew 3000. Balance | 4000 |
| Customer-3 withdrew 3000. Balance | 1000 |
| Customer-4 DENIED. Insufficient funds. Balance | 1000 |
| Customer-5 DENIED. Insufficient funds. Balance | 1000 |
| Final balance | 1000 |
How Intrinsic Locks (Monitors) Work
Thread A wants to enter synchronized block
┌────────────────────────────────────┐
│ synchronized(obj) { │
│ // critical section │←── Thread A ACQUIRES lock
│ } │ Thread B must WAIT
└────────────────────────────────────┘
Lock ownership
┌─────────────┐
│ OBJECT │
│ │
│ Lock: [A] │ ← Thread A holds the lock
│ │
│ Queue: [B] │ ← Thread B waiting to acquire
└─────────────┘
When A exits synchronized block
- Lock released
- B acquires lock and enters
Synchronized Static Methods
public class StaticSync {
private static int globalCounter = 0;
// Lock is on the CLASS object (StaticSync.class)
public static synchronized void increment() {
globalCounter++;
}
// Equivalent to:
public static void incrementExplicit() {
synchronized (StaticSync.class) { // Lock on class
globalCounter++;
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Static Synchronized ===\n");
Thread[] threads = new Thread[4];
for (int i = 0; i < 4; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 50_000; j++) increment();
});
threads[i].start();
}
for (Thread t : threads) t.join();
System.out.println("Expected: 200,000");
System.out.println("Actual: " + globalCounter);
System.out.println("\nNote: Static sync locks on Class object");
System.out.println("Instance sync locks on 'this' (instance)");
System.out.println("They are DIFFERENT locks!");
}
}
=== Static Synchronized ===
Expected: 200,000
Actual: 200,000 ✓
Static synchronized locks on Class object.
The volatile Keyword
volatile ensures visibility of changes across threads (but NOT atomicity):
public class VolatileDemo {
// Without volatile: thread may never see the change!
private static volatile boolean running = true;
public static void main(String[] args) throws InterruptedException {
System.out.println("=== volatile Keyword ===\n");
Thread worker = new Thread(() -> {
long count = 0;
while (running) { // Reads 'running' from main memory (volatile)
count++;
}
System.out.println("[Worker] Stopped after " + count + " iterations");
}, "Worker");
worker.start();
Thread.sleep(100);
System.out.println("[Main] Setting running = false");
running = false; // Written to main memory immediately (volatile)
worker.join();
System.out.println("[Main] Worker has stopped.");
System.out.println("\nWithout volatile:");
System.out.println(" Worker might cache 'running=true' and loop forever!");
System.out.println("With volatile:");
System.out.println(" Every read goes to main memory — sees the change.");
System.out.println("\n⚠️ volatile guarantees VISIBILITY, not ATOMICITY!");
System.out.println(" volatile int x; x++ is still NOT thread-safe!");
}
}
=== volatile Keyword ===
[Main] Sending stop signal...
[Worker] Stopped after 1847293 iterations
Without volatile, worker might never see the stop flag!
Deadlock
Deadlock occurs when two or more threads wait for each other's locks forever:
public class DeadlockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Deadlock Demo ===\n");
Thread t1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("[T1] Holding lockA, waiting for lockB...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) {
System.out.println("[T1] Got both locks!");
}
}
}, "Thread-1");
Thread t2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("[T2] Holding lockB, waiting for lockA...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockA) {
System.out.println("[T2] Got both locks!");
}
}
}, "Thread-2");
t1.start();
t2.start();
// Wait with timeout to detect deadlock
t1.join(3000);
t2.join(3000);
if (t1.isAlive() || t2.isAlive()) {
System.out.println("\n⚠️ DEADLOCK DETECTED!");
System.out.println("T1 state: " + t1.getState());
System.out.println("T2 state: " + t2.getState());
System.out.println("\nBoth threads are BLOCKED waiting for each other.");
// Force exit (in real apps, you'd fix the design)
System.exit(0);
}
}
}
Output:
=== Deadlock Demo ===
[T1] Holding lockA, waiting for lockB...
[T2] Holding lockB, waiting for lockA...
⚠️ DEADLOCK DETECTED!
T1 state: BLOCKED
T2 state: BLOCKED
Both threads are BLOCKED waiting for each other.
Preventing Deadlock: Lock Ordering
public class DeadlockPrevention {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Deadlock Prevention: Consistent Lock Order ===\n");
// SOLUTION: Both threads acquire locks in the SAME order
Thread t1 = new Thread(() -> {
synchronized (lockA) { // Always lockA first
System.out.println("[T1] Got lockA");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) { // Then lockB
System.out.println("[T1] Got lockB — work done!");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lockA) { // Same order: lockA first
System.out.println("[T2] Got lockA");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) { // Then lockB
System.out.println("[T2] Got lockB — work done!");
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("\n✓ No deadlock! Consistent lock ordering prevents it.");
}
}
=== Deadlock Prevention: Consistent Lock Order ===
[T1] Got lockA
[T1] Got lockB
[T1] Done (no deadlock!)
[T2] Got lockA
[T2] Got lockB
[T2] Done (no deadlock!)
Consistent ordering prevents deadlock.
Atomic Variables (java.util.concurrent.atomic)
For simple counters, AtomicInteger is faster than synchronized:
import java.util.concurrent.atomic.*;
public class AtomicDemo {
private static AtomicInteger counter = new AtomicInteger(0);
private static AtomicLong totalBytes = new AtomicLong(0);
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Atomic Variables ===\n");
Thread[] threads = new Thread[4];
for (int i = 0; i < 4; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 50_000; j++) {
counter.incrementAndGet(); // Atomic!
totalBytes.addAndGet(64); // Atomic!
}
});
threads[i].start();
}
for (Thread t : threads) t.join();
System.out.println("Counter: " + counter.get() + " (expected 200,000)");
System.out.println("Bytes: " + totalBytes.get() + " (expected 12,800,000)");
System.out.println("\nAtomic operations:");
System.out.println(" incrementAndGet(), decrementAndGet()");
System.out.println(" addAndGet(delta), compareAndSet(expect, update)");
System.out.println(" getAndIncrement(), getAndAdd(delta)");
System.out.println("\nFaster than synchronized for simple operations!");
}
}
=== Atomic Variables ===
Counter: 200000 (expected 200,000) ✓
Atomic operations are lock-free and thread-safe.
Use AtomicInteger, AtomicLong, AtomicReference for simple shared state.
Common Mistakes
1. Synchronizing on wrong object
// ❌ BAD: Each thread synchronizes on a DIFFERENT lock
public void addItem(String item) {
synchronized (new Object()) { // New object each time!
list.add(item);
}
}
// ✅ GOOD: All threads use the SAME lock
private final Object lock = new Object();
public void addItem(String item) {
synchronized (lock) {
list.add(item);
}
}
2. Over-synchronizing
// ❌ BAD: Entire method synchronized (blocks too long)
public synchronized void processOrder(Order order) {
validateOrder(order); // Doesn't need sync
calculateTotal(order); // Doesn't need sync
updateInventory(order); // Needs sync
sendConfirmation(order); // Doesn't need sync
}
// ✅ GOOD: Only synchronize what's necessary
public void processOrder(Order order) {
validateOrder(order);
calculateTotal(order);
synchronized (inventoryLock) {
updateInventory(order); // Only this needs sync
}
sendConfirmation(order);
}
Interview Questions
Q1: What is a race condition?
Answer: A race condition occurs when two or more threads access shared mutable data concurrently, and the final result depends on the unpredictable timing of thread execution. It causes data corruption and inconsistent behavior.
Q2: What is the difference between synchronized method and synchronized block?
Answer: Synchronized method locks the entire method on this (or Class for static). Synchronized block is more granular — you choose which object to lock on and which code to protect. Block is preferred because it reduces lock contention.
Q3: What is a deadlock? How do you prevent it?
Answer: Deadlock: two or more threads permanently blocked waiting for each other's locks. Prevention: (1) consistent lock ordering, (2) lock timeout (tryLock), (3) avoid nested locks, (4) use concurrent utilities instead of manual locking.
Q4: What is the difference between volatile and synchronized?
Answer: volatile ensures visibility (changes are seen by all threads) but NOT atomicity. synchronized ensures both visibility AND atomicity (mutual exclusion). Use volatile for simple flags; use synchronized for compound operations.
Q5: What is an intrinsic lock (monitor)?
Answer: Every Java object has an intrinsic lock (monitor). When a thread enters a synchronized block/method, it acquires the object's lock. Other threads trying to acquire the same lock are blocked until it's released. Only one thread holds a given lock at a time.
Q6: What is the difference between synchronized(this) and synchronized(SomeClass.class)?
Answer: synchronized(this) locks on the current instance — different instances have different locks. synchronized(SomeClass.class) locks on the Class object — there's only one, so all instances share the same lock. Use class-level for static shared resources.
Q8: What is the difference between synchronized method and synchronized block?
A: Synchronized method locks the entire object (this) for instance methods or the Class object for static methods. Synchronized block can lock on any object and synchronize only the critical section. Block is preferred because: (1) finer granularity, (2) less lock contention, (3) can use different lock objects for different critical sections.
Q9: What is a deadlock and how do you prevent it?
A: Deadlock occurs when two or more threads are waiting for each other's locks forever. Prevention strategies: (1) Lock ordering — always acquire locks in the same order. (2) Lock timeout — use tryLock(timeout) with ReentrantLock. (3) Single lock — use one lock for all shared resources. (4) Avoid nested locks when possible.
Q10: What is the difference between object-level and class-level locking?
A: Object-level lock (synchronized on this): each instance has its own lock — different instances can be accessed concurrently. Class-level lock (synchronized on ClassName.class or static synchronized method): one lock for ALL instances — no two threads can execute any static synchronized method simultaneously.
Q11: What are the alternatives to synchronized in Java?
A: (1) ReentrantLock — explicit lock/unlock, supports fairness, tryLock, interruptible. (2) ReadWriteLock — allows concurrent reads. (3) StampedLock — optimistic reading. (4) Atomic* classes — lock-free operations. (5) volatile — visibility without atomicity. (6) ConcurrentHashMap — built-in thread safety.
Q12: Does synchronization guarantee visibility of changes across threads?
A: Yes! The Java Memory Model guarantees that when a thread releases a lock (exits synchronized), all its writes become visible to the thread that subsequently acquires the same lock. This "happens-before" relationship ensures both mutual exclusion AND memory visibility.