Synchronization in Java
Learn how to prevent data corruption when multiple threads access shared resources
Think of synchronization like a bathroom with a lock! When someone is using the bathroom (a thread working with shared data), they lock the door so nobody else can come in at the same time. Only after they're done and unlock the door can the next person enter. Without this lock, multiple people trying to use the bathroom at once would be chaos - just like threads accessing shared data without synchronization!
The Problem Without Synchronization
Imagine a bank account shared by family members. If two people try to withdraw money at exactly the same time without proper coordination, the balance could get messed up! Let's see this race condition in code:
// Without synchronization - race condition occurs!class BankAccount { private int balance = 1000; public void withdraw(int amount) { System.out.println(Thread.currentThread().getName() + " is trying to withdraw $" + amount); // Check if enough balance if (balance >= amount) { System.out.println(Thread.currentThread().getName() + " - Balance is sufficient"); try { // Simulate some processing time Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // Withdraw the money balance = balance - amount; System.out.println(Thread.currentThread().getName() + " completed withdrawal. Remaining balance: $" + balance); } else { System.out.println(Thread.currentThread().getName() + " - Insufficient balance!"); } } public int getBalance() { return balance; }}public class RaceConditionExample { public static void main(String[] args) { BankAccount account = new BankAccount(); // Two family members try to withdraw money simultaneously Thread husband = new Thread(() -> { account.withdraw(800); }, "Husband"); Thread wife = new Thread(() -> { account.withdraw(800); }, "Wife"); husband.start(); wife.start(); try { husband.join(); wife.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("\nFinal balance: $" + account.getBalance()); }}/* Output (Race Condition!):Husband is trying to withdraw $800Wife is trying to withdraw $800Husband - Balance is sufficientWife - Balance is sufficientHusband completed withdrawal. Remaining balance: $200Wife completed withdrawal. Remaining balance: $-600Final balance: $-600PROBLEM: Both checked balance before either withdrew!This should never happen - you can't have negative money!*/Synchronized Methods
The easiest way to fix this is by using synchronized methods! When you add the 'synchronized' keyword to a method, only one thread can execute it at a time. It's like putting a lock on the bathroom door - one person at a time!
// Using synchronized method - problem solved!class SafeBankAccount { private int balance = 1000; // synchronized keyword ensures only one thread at a time public synchronized void withdraw(int amount) { System.out.println(Thread.currentThread().getName() + " is trying to withdraw $" + amount); if (balance >= amount) { System.out.println(Thread.currentThread().getName() + " - Balance is sufficient"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } balance = balance - amount; System.out.println(Thread.currentThread().getName() + " completed withdrawal. Remaining balance: $" + balance); } else { System.out.println(Thread.currentThread().getName() + " - Insufficient balance!"); } } public synchronized int getBalance() { return balance; }}public class SynchronizedMethodExample { public static void main(String[] args) { SafeBankAccount account = new SafeBankAccount(); Thread husband = new Thread(() -> { account.withdraw(800); }, "Husband"); Thread wife = new Thread(() -> { account.withdraw(800); }, "Wife"); husband.start(); wife.start(); try { husband.join(); wife.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("\nFinal balance: $" + account.getBalance()); }}/* Output (Fixed!):Husband is trying to withdraw $800Husband - Balance is sufficientHusband completed withdrawal. Remaining balance: $200Wife is trying to withdraw $800Wife - Insufficient balance!Final balance: $200SUCCESS: Wife waited for Husband to finish first!Now only one person could withdraw, preventing negative balance.*/Synchronized Blocks
Sometimes you don't want to lock the entire method - just a specific part. That's where synchronized blocks come in! It's like having a shared kitchen but only locking the refrigerator when someone needs to use it, allowing others to use the stove at the same time.
// Using synchronized blocks for finer controlclass Restaurant { private int totalOrders = 0; private final Object orderLock = new Object(); private final Object cookLock = new Object(); public void processOrder(String dish) { // This part doesn't need synchronization System.out.println(Thread.currentThread().getName() + " received order for: " + dish); try { Thread.sleep(50); // Simulate order entry } catch (InterruptedException e) { e.printStackTrace(); } // Only synchronize the critical section synchronized (orderLock) { totalOrders++; System.out.println(Thread.currentThread().getName() + " - Total orders now: " + totalOrders); } // Cooking can happen in parallel for different dishes synchronized (cookLock) { System.out.println(Thread.currentThread().getName() + " is cooking: " + dish); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(dish + " is ready!"); } } public int getTotalOrders() { synchronized (orderLock) { return totalOrders; } }}public class SynchronizedBlockExample { public static void main(String[] args) { Restaurant restaurant = new Restaurant(); Thread waiter1 = new Thread(() -> { restaurant.processOrder("Pizza"); }, "Waiter-1"); Thread waiter2 = new Thread(() -> { restaurant.processOrder("Burger"); }, "Waiter-2"); Thread waiter3 = new Thread(() -> { restaurant.processOrder("Pasta"); }, "Waiter-3"); waiter1.start(); waiter2.start(); waiter3.start(); try { waiter1.join(); waiter2.join(); waiter3.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("\nTotal orders processed: " + restaurant.getTotalOrders()); }}/* Output:Waiter-1 received order for: PizzaWaiter-2 received order for: BurgerWaiter-3 received order for: PastaWaiter-1 - Total orders now: 1Waiter-1 is cooking: PizzaWaiter-2 - Total orders now: 2Waiter-3 - Total orders now: 3Pizza is ready!Waiter-2 is cooking: BurgerBurger is ready!Waiter-3 is cooking: PastaPasta is ready!Total orders processed: 3*/Static Synchronization
When you need to synchronize across all instances of a class (not just one object), use static synchronization. This locks the entire class, like having one master key that controls all apartments in a building, not just one apartment!
// Static synchronization - class-level lockingclass Counter { private static int count = 0; // Synchronized on the class itself public static synchronized void increment() { count++; System.out.println(Thread.currentThread().getName() + " incremented count to: " + count); } // Alternative: synchronized block with class lock public static void decrement() { synchronized (Counter.class) { count--; System.out.println(Thread.currentThread().getName() + " decremented count to: " + count); } } public static synchronized int getCount() { return count; }}public class StaticSyncExample { public static void main(String[] args) { // Create multiple threads that all work on the static counter Runnable incrementTask = () -> { for (int i = 0; i < 5; i++) { Counter.increment(); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } }; Runnable decrementTask = () -> { for (int i = 0; i < 3; i++) { Counter.decrement(); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } }; Thread t1 = new Thread(incrementTask, "Thread-1"); Thread t2 = new Thread(incrementTask, "Thread-2"); Thread t3 = new Thread(decrementTask, "Thread-3"); t1.start(); t2.start(); t3.start(); try { t1.join(); t2.join(); t3.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("\nFinal count: " + Counter.getCount()); }}/* Output:Thread-1 incremented count to: 1Thread-2 incremented count to: 2Thread-3 decremented count to: 1Thread-1 incremented count to: 2Thread-2 incremented count to: 3Thread-3 decremented count to: 2Thread-1 incremented count to: 3Thread-2 incremented count to: 4Thread-3 decremented count to: 3Thread-1 incremented count to: 4Thread-2 incremented count to: 5Thread-1 incremented count to: 6Thread-2 incremented count to: 7Final count: 7(5 + 5 - 3 = 7) ✓ Correctly synchronized!*/Understanding Deadlock
Deadlock happens when two or more threads are waiting for each other to release locks, and nobody can proceed! It's like two people meeting in a narrow hallway, each waiting for the other to move first - they're stuck forever! Let's see this dangerous situation:
// Example of Deadlock - Be careful!class Resource { private String name; public Resource(String name) { this.name = name; } public String getName() { return name; }}public class DeadlockExample { public static void main(String[] args) { Resource pen = new Resource("Pen"); Resource paper = new Resource("Paper"); // Thread 1: Takes pen, then wants paper Thread student1 = new Thread(() -> { synchronized (pen) { System.out.println("Student-1 acquired: " + pen.getName()); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Student-1 waiting for: " + paper.getName()); synchronized (paper) { System.out.println("Student-1 acquired: " + paper.getName()); System.out.println("Student-1 writing..."); } } }, "Student-1"); // Thread 2: Takes paper, then wants pen Thread student2 = new Thread(() -> { synchronized (paper) { System.out.println("Student-2 acquired: " + paper.getName()); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Student-2 waiting for: " + pen.getName()); synchronized (pen) { System.out.println("Student-2 acquired: " + pen.getName()); System.out.println("Student-2 writing..."); } } }, "Student-2"); student1.start(); student2.start(); // This program will hang forever! }}/* Output (Deadlock - program hangs!):Student-1 acquired: PenStudent-2 acquired: PaperStudent-1 waiting for: PaperStudent-2 waiting for: Pen(Program stuck here forever - both waiting for each other!)SOLUTION: Always acquire locks in the same order!If both students always get pen first, then paper, no deadlock.*/Real-World Example: Thread-Safe Shopping Cart
Let's build a practical shopping cart that multiple threads can safely use at the same time. This shows how to properly synchronize a real application where multiple users might add or remove items simultaneously.
// Real-world thread-safe shopping cartimport java.util.HashMap;import java.util.Map;class ShoppingCart { private Map<String, Integer> items = new HashMap<>(); private double totalPrice = 0.0; // Synchronized method to add items public synchronized void addItem(String itemName, int quantity, double price) { System.out.println(Thread.currentThread().getName() + " adding: " + quantity + "x " + itemName); items.put(itemName, items.getOrDefault(itemName, 0) + quantity); totalPrice += (quantity * price); System.out.println(Thread.currentThread().getName() + " - Cart updated. Total: $" + String.format("%.2f", totalPrice)); } // Synchronized method to remove items public synchronized boolean removeItem(String itemName, int quantity) { System.out.println(Thread.currentThread().getName() + " removing: " + quantity + "x " + itemName); Integer currentQuantity = items.get(itemName); if (currentQuantity == null || currentQuantity < quantity) { System.out.println(Thread.currentThread().getName() + " - Cannot remove. Item not in cart or insufficient quantity."); return false; } if (currentQuantity == quantity) { items.remove(itemName); } else { items.put(itemName, currentQuantity - quantity); } System.out.println(Thread.currentThread().getName() + " - Item removed successfully"); return true; } // Synchronized method to get cart summary public synchronized void printCart() { System.out.println("\n=== Shopping Cart Summary ==="); System.out.println("Customer: " + Thread.currentThread().getName()); if (items.isEmpty()) { System.out.println("Cart is empty"); } else { items.forEach((item, qty) -> System.out.println(" " + item + ": " + qty)); } System.out.println("Total Price: $" + String.format("%.2f", totalPrice)); System.out.println("============================\n"); } public synchronized int getItemCount() { return items.size(); }}public class ShoppingCartExample { public static void main(String[] args) { ShoppingCart cart = new ShoppingCart(); // Multiple users adding items simultaneously Thread user1 = new Thread(() -> { cart.addItem("Laptop", 1, 999.99); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } cart.addItem("Mouse", 2, 25.50); }, "User-1"); Thread user2 = new Thread(() -> { cart.addItem("Keyboard", 1, 79.99); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } cart.addItem("Monitor", 1, 299.99); }, "User-2"); Thread user3 = new Thread(() -> { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } cart.removeItem("Mouse", 1); }, "User-3"); user1.start(); user2.start(); user3.start(); try { user1.join(); user2.join(); user3.join(); } catch (InterruptedException e) { e.printStackTrace(); } cart.printCart(); }}/* Output:User-1 adding: 1x LaptopUser-1 - Cart updated. Total: $999.99User-2 adding: 1x KeyboardUser-2 - Cart updated. Total: $1079.98User-1 adding: 2x MouseUser-1 - Cart updated. Total: $1130.98User-2 adding: 1x MonitorUser-2 - Cart updated. Total: $1430.97User-3 removing: 1x MouseUser-3 - Item removed successfully=== Shopping Cart Summary ===Customer: main Laptop: 1 Keyboard: 1 Mouse: 1 Monitor: 1Total Price: $1405.47============================All operations thread-safe! No data corruption.*/Key Concepts
Race Condition
Occurs when multiple threads access shared data simultaneously and the final outcome depends on the timing of their execution. Synchronization prevents this by ensuring only one thread accesses the critical section at a time.
Monitor Lock
Every Java object has an intrinsic lock (monitor). When a thread enters a synchronized method or block, it acquires this lock. Other threads must wait until the lock is released.
Synchronized vs Blocks
Synchronized methods lock the entire method. Synchronized blocks allow finer control - you can lock only specific code sections or use different objects as locks, improving performance.
Deadlock Prevention
To prevent deadlock: always acquire locks in the same order, avoid nested locks when possible, use timeout mechanisms, and minimize the time locks are held.
Best Practices
- ✓Always synchronize access to shared mutable data to prevent race conditions
- ✓Keep synchronized blocks as small as possible - only protect the critical section
- ✓Avoid calling other synchronized methods from within a synchronized method to prevent deadlock
- ✓Always acquire multiple locks in the same order across all threads
- ✓Document which locks protect which data fields clearly in comments
- ✓Consider using higher-level concurrency utilities (java.util.concurrent) instead of basic synchronization
Common Mistakes to Avoid
- ✗Synchronizing on 'this' when the object is exposed to external code - use a private lock object instead
- ✗Making entire methods synchronized when only a small section needs protection
- ✗Forgetting to synchronize all access points to shared data - must synchronize both reads and writes
- ✗Creating deadlocks by acquiring locks in different orders in different threads
- ✗Using synchronized blocks with different lock objects thinking they protect the same data
- ✗Over-synchronizing everything - this hurts performance and may not be necessary
Interview Tips
- 💡Know the difference between synchronized methods and synchronized blocks - when to use each
- 💡Be able to identify and explain race conditions in code examples
- 💡Understand what causes deadlock and how to prevent it (consistent lock ordering)
- 💡Know that synchronization uses monitor locks - every object has one intrinsic lock
- 💡Understand that synchronized methods lock 'this' for instance methods and the Class object for static methods
- 💡Be prepared to write thread-safe code and explain why synchronization is needed in specific scenarios