Overview
The synchronized keyword in Java is used to control access to critical sections of code by multiple threads. It ensures that only one thread can execute a synchronized block/method at a time.
1. Synchronized Methods
Instance Method Synchronization
public class SynchronizedMethods { static class Counter { private int count = 0; // Synchronized instance method public synchronized void increment() { count++; System.out.println(Thread.currentThread().getName() + " incremented to: " + count); } // Synchronized instance method public synchronized void decrement() { count--; System.out.println(Thread.currentThread().getName() + " decremented to: " + count); } public synchronized int getCount() { return count; } } static class CounterTask implements Runnable { private Counter counter; private boolean increment; public CounterTask(Counter counter, boolean increment) { this.counter = counter; this.increment = increment; } @Override public void run() { for (int i = 0; i < 5; i++) { if (increment) { counter.increment(); } else { counter.decrement(); } try { Thread.sleep(100); // Simulate some work } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } public static void main(String[] args) throws InterruptedException { System.out.println("=== Synchronized Instance Methods ==="); Counter counter = new Counter(); Thread t1 = new Thread(new CounterTask(counter, true), "Increment-Thread"); Thread t2 = new Thread(new CounterTask(counter, false), "Decrement-Thread"); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("Final count: " + counter.getCount()); } } Static Method Synchronization
public class StaticSynchronization { static class SharedResource { private static int staticCounter = 0; private int instanceCounter = 0; // Synchronized static method - locks on Class object public static synchronized void incrementStatic() { staticCounter++; System.out.println(Thread.currentThread().getName() + " - Static counter: " + staticCounter); try { Thread.sleep(100); // Simulate work } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } // Synchronized instance method - locks on instance public synchronized void incrementInstance() { instanceCounter++; System.out.println(Thread.currentThread().getName() + " - Instance counter: " + instanceCounter); try { Thread.sleep(100); // Simulate work } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } public static void main(String[] args) throws InterruptedException { System.out.println("=== Static vs Instance Synchronization ==="); SharedResource resource1 = new SharedResource(); SharedResource resource2 = new SharedResource(); // Threads using static synchronization (class-level lock) Thread t1 = new Thread(() -> { for (int i = 0; i < 3; i++) { SharedResource.incrementStatic(); } }, "Static-Thread-1"); Thread t2 = new Thread(() -> { for (int i = 0; i < 3; i++) { SharedResource.incrementStatic(); } }, "Static-Thread-2"); // Threads using instance synchronization (instance-level lock) Thread t3 = new Thread(() -> { for (int i = 0; i < 3; i++) { resource1.incrementInstance(); } }, "Instance-Thread-1"); Thread t4 = new Thread(() -> { for (int i = 0; i < 3; i++) { resource2.incrementInstance(); } }, "Instance-Thread-2"); t1.start(); t2.start(); t3.start(); t4.start(); t1.join(); t2.join(); t3.join(); t4.join(); System.out.println("All threads completed"); } } 2. Synchronized Blocks
Instance Synchronized Blocks
public class SynchronizedBlocks { static class BankAccount { private double balance; private final Object lock = new Object(); private String accountHolder; public BankAccount(String accountHolder, double initialBalance) { this.accountHolder = accountHolder; this.balance = initialBalance; } // Using synchronized block with 'this' public void deposit(double amount) { synchronized (this) { balance += amount; System.out.println(Thread.currentThread().getName() + " deposited: " + amount + ", Balance: " + balance); // Simulate some processing time try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } // Using synchronized block with custom lock object public void withdraw(double amount) { synchronized (lock) { if (balance >= amount) { balance -= amount; System.out.println(Thread.currentThread().getName() + " withdrew: " + amount + ", Balance: " + balance); } else { System.out.println(Thread.currentThread().getName() + " - Insufficient funds for withdrawal: " + amount); } try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } // Different operations can use different locks public void updateAccountHolder(String newHolder) { synchronized (lock) { // Using custom lock String oldHolder = this.accountHolder; this.accountHolder = newHolder; System.out.println(Thread.currentThread().getName() + " updated holder from " + oldHolder + " to " + newHolder); try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } public double getBalance() { synchronized (this) { // Using 'this' lock return balance; } } } public static void main(String[] args) throws InterruptedException { System.out.println("=== Synchronized Blocks ==="); BankAccount account = new BankAccount("John Doe", 1000); // Create multiple threads performing different operations Thread depositThread1 = new Thread(() -> { for (int i = 0; i < 3; i++) { account.deposit(100); } }, "Depositor-1"); Thread depositThread2 = new Thread(() -> { for (int i = 0; i < 3; i++) { account.deposit(50); } }, "Depositor-2"); Thread withdrawThread = new Thread(() -> { for (int i = 0; i < 4; i++) { account.withdraw(200); } }, "Withdrawer"); Thread updateThread = new Thread(() -> { account.updateAccountHolder("Jane Smith"); }, "Updater"); depositThread1.start(); depositThread2.start(); withdrawThread.start(); updateThread.start(); depositThread1.join(); depositThread2.join(); withdrawThread.join(); updateThread.join(); System.out.println("Final balance: " + account.getBalance()); } } Static Synchronized Blocks
public class StaticSynchronizedBlocks { static class ConfigurationManager { private static Map<String, String> config = new HashMap<>(); private static final Object staticLock = new Object(); // Static synchronized block public static void updateConfig(String key, String value) { synchronized (staticLock) { String oldValue = config.put(key, value); System.out.println(Thread.currentThread().getName() + " updated " + key + " from '" + oldValue + "' to '" + value + "'"); try { Thread.sleep(100); // Simulate work } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } // Another static method using class-level lock public static void clearConfig() { synchronized (ConfigurationManager.class) { int size = config.size(); config.clear(); System.out.println(Thread.currentThread().getName() + " cleared " + size + " config entries"); try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } public static String getConfig(String key) { synchronized (staticLock) { return config.get(key); } } } public static void main(String[] args) throws InterruptedException { System.out.println("=== Static Synchronized Blocks ==="); // Multiple threads updating static configuration Thread t1 = new Thread(() -> { for (int i = 0; i < 3; i++) { ConfigurationManager.updateConfig("server" + i, "192.168.1." + i); } }, "Config-Updater-1"); Thread t2 = new Thread(() -> { for (int i = 3; i < 6; i++) { ConfigurationManager.updateConfig("server" + i, "192.168.1." + i); } }, "Config-Updater-2"); Thread t3 = new Thread(() -> { ConfigurationManager.clearConfig(); }, "Config-Cleaner"); t1.start(); t2.start(); Thread.sleep(200); // Let some updates happen t3.start(); t1.join(); t2.join(); t3.join(); System.out.println("Configuration management completed"); } } 3. Producer-Consumer Problem
Classic Producer-Consumer with Synchronization
public class ProducerConsumer { static class MessageQueue { private final Queue<String> queue = new LinkedList<>(); private final int capacity; public MessageQueue(int capacity) { this.capacity = capacity; } public synchronized void produce(String message) throws InterruptedException { while (queue.size() == capacity) { System.out.println(Thread.currentThread().getName() + " - Queue full, waiting..."); wait(); // Wait until consumer notifies } queue.add(message); System.out.println(Thread.currentThread().getName() + " produced: " + message + " | Queue size: " + queue.size()); notifyAll(); // Notify waiting consumers } public synchronized String consume() throws InterruptedException { while (queue.isEmpty()) { System.out.println(Thread.currentThread().getName() + " - Queue empty, waiting..."); wait(); // Wait until producer notifies } String message = queue.remove(); System.out.println(Thread.currentThread().getName() + " consumed: " + message + " | Queue size: " + queue.size()); notifyAll(); // Notify waiting producers return message; } public synchronized int size() { return queue.size(); } } static class Producer implements Runnable { private MessageQueue queue; private int messageCount; public Producer(MessageQueue queue, int messageCount) { this.queue = queue; this.messageCount = messageCount; } @Override public void run() { try { for (int i = 1; i <= messageCount; i++) { String message = "Message-" + i + "-from-" + Thread.currentThread().getName(); queue.produce(message); // Simulate production time Thread.sleep((long) (Math.random() * 200)); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } static class Consumer implements Runnable { private MessageQueue queue; private int messageCount; public Consumer(MessageQueue queue, int messageCount) { this.queue = queue; this.messageCount = messageCount; } @Override public void run() { try { for (int i = 1; i <= messageCount; i++) { queue.consume(); // Simulate consumption time Thread.sleep((long) (Math.random() * 300)); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } public static void main(String[] args) throws InterruptedException { System.out.println("=== Producer-Consumer with Synchronization ==="); MessageQueue queue = new MessageQueue(3); Thread producer1 = new Thread(new Producer(queue, 5), "Producer-1"); Thread producer2 = new Thread(new Producer(queue, 5), "Producer-2"); Thread consumer1 = new Thread(new Consumer(queue, 7), "Consumer-1"); Thread consumer2 = new Thread(new Consumer(queue, 3), "Consumer-2"); producer1.start(); producer2.start(); consumer1.start(); consumer2.start(); producer1.join(); producer2.join(); consumer1.join(); consumer2.join(); System.out.println("Final queue size: " + queue.size()); System.out.println("Producer-Consumer completed"); } } 4. Deadlock Prevention
Deadlock Example and Prevention
public class DeadlockPrevention { static class Account { private final String id; private double balance; public Account(String id, double balance) { this.id = id; this.balance = balance; } public String getId() { return id; } public synchronized void debit(double amount) { balance -= amount; } public synchronized void credit(double amount) { balance += amount; } public synchronized double getBalance() { return balance; } // Potential deadlock method public void transferWithDeadlock(Account to, double amount) { synchronized (this) { System.out.println(Thread.currentThread().getName() + " acquired lock on " + this.id); try { Thread.sleep(100); } catch (InterruptedException e) {} synchronized (to) { System.out.println(Thread.currentThread().getName() + " acquired lock on " + to.id); if (this.balance >= amount) { this.debit(amount); to.credit(amount); System.out.println("Transferred " + amount + " from " + this.id + " to " + to.id); } } } } // Deadlock prevention using ordered locking public void transferSafe(Account to, double amount) { // Determine lock order based on account ID Account firstLock = this.id.compareTo(to.id) < 0 ? this : to; Account secondLock = this.id.compareTo(to.id) < 0 ? to : this; synchronized (firstLock) { System.out.println(Thread.currentThread().getName() + " acquired lock on " + firstLock.id); try { Thread.sleep(100); } catch (InterruptedException e) {} synchronized (secondLock) { System.out.println(Thread.currentThread().getName() + " acquired lock on " + secondLock.id); if (this.balance >= amount) { this.debit(amount); to.credit(amount); System.out.println("Transferred " + amount + " from " + this.id + " to " + to.id); } } } } } public static void main(String[] args) throws InterruptedException { System.out.println("=== Deadlock Prevention ==="); Account accountA = new Account("A", 1000); Account accountB = new Account("B", 1000); // This might cause deadlock System.out.println("\n--- Potential Deadlock Scenario ---"); Thread t1 = new Thread(() -> { accountA.transferWithDeadlock(accountB, 100); }, "Thread-1-A-to-B"); Thread t2 = new Thread(() -> { accountB.transferWithDeadlock(accountA, 50); }, "Thread-2-B-to-A"); t1.start(); t2.start(); // Wait a bit to see if deadlock occurs Thread.sleep(1000); if (t1.isAlive() && t2.isAlive()) { System.out.println("Deadlock detected! Stopping threads..."); t1.interrupt(); t2.interrupt(); } t1.join(); t2.join(); // Safe transfer System.out.println("\n--- Safe Transfer Scenario ---"); Thread t3 = new Thread(() -> { accountA.transferSafe(accountB, 100); }, "Thread-3-A-to-B"); Thread t4 = new Thread(() -> { accountB.transferSafe(accountA, 50); }, "Thread-4-B-to-A"); t3.start(); t4.start(); t3.join(); t4.join(); System.out.println("Account A balance: " + accountA.getBalance()); System.out.println("Account B balance: " + accountB.getBalance()); } } 5. Read-Write Locks Pattern
Synchronized Read-Write Implementation
public class ReadWriteSynchronized { static class SharedDictionary { private final Map<String, String> dictionary = new HashMap<>(); private int readCount = 0; private int writeCount = 0; public synchronized void put(String key, String value) { writeCount++; System.out.println(Thread.currentThread().getName() + " writing - Key: " + key + ", Value: " + value + " (Write #" + writeCount + ")"); // Simulate write operation time try { Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } dictionary.put(key, value); System.out.println(Thread.currentThread().getName() + " write completed"); } public synchronized String get(String key) { readCount++; System.out.println(Thread.currentThread().getName() + " reading - Key: " + key + " (Read #" + readCount + ")"); // Simulate read operation time try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } String value = dictionary.get(key); System.out.println(Thread.currentThread().getName() + " read - Key: " + key + ", Value: " + value); return value; } public synchronized int size() { return dictionary.size(); } } static class Writer implements Runnable { private SharedDictionary dictionary; private int entries; public Writer(SharedDictionary dictionary, int entries) { this.dictionary = dictionary; this.entries = entries; } @Override public void run() { for (int i = 0; i < entries; i++) { String key = "key-" + Thread.currentThread().getName() + "-" + i; String value = "value-" + i; dictionary.put(key, value); try { Thread.sleep(150); // Time between writes } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } } static class Reader implements Runnable { private SharedDictionary dictionary; private int reads; public Reader(SharedDictionary dictionary, int reads) { this.dictionary = dictionary; this.reads = reads; } @Override public void run() { for (int i = 0; i < reads; i++) { String key = "key-" + (i % 5); // Read from limited set of keys dictionary.get(key); try { Thread.sleep(50); // Time between reads } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } } public static void main(String[] args) throws InterruptedException { System.out.println("=== Read-Write with Synchronization ==="); SharedDictionary dictionary = new SharedDictionary(); // Create writers Thread writer1 = new Thread(new Writer(dictionary, 3), "Writer-1"); Thread writer2 = new Thread(new Writer(dictionary, 3), "Writer-2"); // Create readers Thread reader1 = new Thread(new Reader(dictionary, 5), "Reader-1"); Thread reader2 = new Thread(new Reader(dictionary, 5), "Reader-2"); Thread reader3 = new Thread(new Reader(dictionary, 5), "Reader-3"); writer1.start(); writer2.start(); reader1.start(); reader2.start(); reader3.start(); writer1.join(); writer2.join(); reader1.join(); reader2.join(); reader3.join(); System.out.println("Final dictionary size: " + dictionary.size()); } } 6. Double-Checked Locking
Singleton Pattern with Synchronization
public class DoubleCheckedLocking { // Singleton class with various synchronization approaches static class Singleton { private static Singleton instance; private static final Object lock = new Object(); private int value; private Singleton() { // Private constructor value = 42; System.out.println("Singleton instance created"); } // Method 1: Synchronized method (thread-safe but slow) public static synchronized Singleton getInstanceSyncMethod() { if (instance == null) { instance = new Singleton(); } return instance; } // Method 2: Synchronized block (better performance) public static Singleton getInstanceSyncBlock() { synchronized (lock) { if (instance == null) { instance = new Singleton(); } return instance; } } // Method 3: Double-checked locking (best performance) public static Singleton getInstanceDoubleChecked() { if (instance == null) { // First check (no synchronization) synchronized (lock) { if (instance == null) { // Second check (with synchronization) instance = new Singleton(); } } } return instance; } // Method 4: Initialization-on-demand holder idiom (recommended) private static class Holder { static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstanceHolder() { return Holder.INSTANCE; } public int getValue() { return value; } public void setValue(int value) { this.value = value; } } static class SingletonAccessor implements Runnable { private int methodType; public SingletonAccessor(int methodType) { this.methodType = methodType; } @Override public void run() { Singleton singleton = null; switch (methodType) { case 1: singleton = Singleton.getInstanceSyncMethod(); break; case 2: singleton = Singleton.getInstanceSyncBlock(); break; case 3: singleton = Singleton.getInstanceDoubleChecked(); break; case 4: singleton = Singleton.getInstanceHolder(); break; } System.out.println(Thread.currentThread().getName() + " got singleton with value: " + singleton.getValue()); // Modify value to verify we're using same instance singleton.setValue(singleton.getValue() + 1); } } public static void main(String[] args) throws InterruptedException { System.out.println("=== Singleton Synchronization Patterns ==="); // Test each synchronization method for (int method = 1; method <= 4; method++) { System.out.println("\n--- Testing Method " + method + " ---"); // Reset singleton for each test try { java.lang.reflect.Field field = Singleton.class.getDeclaredField("instance"); field.setAccessible(true); field.set(null, null); } catch (Exception e) { e.printStackTrace(); } Thread[] threads = new Thread[5]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(new SingletonAccessor(method), "Thread-" + (i + 1)); } for (Thread thread : threads) { thread.start(); } for (Thread thread : threads) { thread.join(); } // Verify final value Singleton finalInstance = null; switch (method) { case 1: finalInstance = Singleton.getInstanceSyncMethod(); break; case 2: finalInstance = Singleton.getInstanceSyncBlock(); break; case 3: finalInstance = Singleton.getInstanceDoubleChecked(); break; case 4: finalInstance = Singleton.getInstanceHolder(); break; } System.out.println("Final singleton value: " + finalInstance.getValue()); } } } 7. Best Practices and Performance
Synchronization Best Practices
public class SynchronizationBestPractices { // 1. Use private final lock objects static class PrivateLock { private final Object lock = new Object(); private int data; public void updateData() { synchronized (lock) { // Good: using private lock data++; System.out.println("Data updated to: " + data); } } } // 2. Avoid synchronizing on non-final objects static class NonFinalLock { private Object lock = new Object(); // Bad: non-final public void badMethod() { synchronized (lock) { // lock could be reassigned, breaking synchronization } } } // 3. Keep synchronized blocks small static class SmallCriticalSections { private int counter; private final Object lock = new Object(); // Good: small synchronized block public void efficientIncrement() { // Non-critical work outside synchronized block performValidation(); synchronized (lock) { // Only critical section is synchronized counter++; } // More non-critical work performLogging(); } // Bad: large synchronized block public void inefficientIncrement() { synchronized (lock) { performValidation(); // This doesn't need synchronization counter++; performLogging(); // This doesn't need synchronization } } private void performValidation() { // Simulate validation work try { Thread.sleep(10); } catch (InterruptedException e) {} } private void performLogging() { // Simulate logging work try { Thread.sleep(10); } catch (InterruptedException e) {} } } // 4. Use different locks for independent operations static class MultipleLocks { private final Object readLock = new Object(); private final Object writeLock = new Object(); private int readCount = 0; private int writeCount = 0; public void readOperation() { synchronized (readLock) { readCount++; System.out.println("Read operation #" + readCount); } } public void writeOperation() { synchronized (writeLock) { writeCount++; System.out.println("Write operation #" + writeCount); } } } // 5. Avoid nested synchronization (deadlock risk) static class NestedSynchronization { private final Object lock1 = new Object(); private final Object lock2 = new Object(); // Dangerous: nested synchronization public void dangerousMethod() { synchronized (lock1) { System.out.println("Acquired lock1"); try { Thread.sleep(100); } catch (InterruptedException e) {} synchronized (lock2) { // Potential deadlock System.out.println("Acquired lock2"); } } } // Safe: use ordered locking public void safeMethod(Object obj1, Object obj2) { Object firstLock = System.identityHashCode(obj1) < System.identityHashCode(obj2) ? obj1 : obj2; Object secondLock = System.identityHashCode(obj1) < System.identityHashCode(obj2) ? obj2 : obj1; synchronized (firstLock) { synchronized (secondLock) { // Critical section } } } } public static void main(String[] args) throws InterruptedException { System.out.println("=== Synchronization Best Practices ==="); // Test efficient vs inefficient synchronization SmallCriticalSections example = new SmallCriticalSections(); System.out.println("Testing efficient synchronization..."); long start = System.currentTimeMillis(); Thread t1 = new Thread(() -> { for (int i = 0; i < 10; i++) example.efficientIncrement(); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 10; i++) example.efficientIncrement(); }); t1.start(); t2.start(); t1.join(); t2.join(); long efficientTime = System.currentTimeMillis() - start; System.out.println("Testing inefficient synchronization..."); start = System.currentTimeMillis(); Thread t3 = new Thread(() -> { for (int i = 0; i < 10; i++) example.inefficientIncrement(); }); Thread t4 = new Thread(() -> { for (int i = 0; i < 10; i++) example.inefficientIncrement(); }); t3.start(); t4.start(); t3.join(); t4.join(); long inefficientTime = System.currentTimeMillis() - start; System.out.println("Efficient time: " + efficientTime + "ms"); System.out.println("Inefficient time: " + inefficientTime + "ms"); System.out.println("Improvement: " + ((inefficientTime - efficientTime) * 100.0 / inefficientTime) + "%"); } } Key Points Summary
- Synchronized Methods: Lock on
this(instance) orClassobject (static) - Synchronized Blocks: More flexible, can use any object as lock
- Intrinsic Locks: Every object has an intrinsic lock (monitor)
- Reentrant: Same thread can acquire the same lock multiple times
- Memory Visibility: Synchronization ensures visibility changes across threads
Best Practices
- Use private final lock objects instead of synchronizing on
this - Keep synchronized blocks small to minimize contention
- Use different locks for independent operations
- Avoid nested synchronization to prevent deadlocks
- Consider higher-level concurrency utilities (
java.util.concurrent) for complex scenarios
Synchronization is fundamental for thread safety in Java, but should be used judiciously to avoid performance issues and deadlocks.