Java Thread Synchronization

Learn how to coordinate multiple threads and prevent data inconsistency in Java multithreading.

What is Thread Synchronization?

Thread synchronization is the process of controlling access to shared resources by multiple threads. When multiple threads try to access the same resource simultaneously, it can lead to data inconsistency. Synchronization ensures that only one thread can access the resource at a time.

Think of it like a bathroom with one door - only one person can use it at a time. Others must wait outside until it's free.

Why Do We Need Synchronization? Without synchronization, multiple threads accessing shared data can cause race conditions, leading to unpredictable results and data corruption.

Problem Without Synchronization

Let's see what happens when multiple threads access shared data without synchronization:

Example: Without Synchronization (Race Condition)

class Counter {
    int count = 0;
    
    public void increment() {
        count++;  // Not thread-safe
    }
}

class MyThread extends Thread {
    Counter counter;
    
    MyThread(Counter counter) {
        this.counter = counter;
    }
    
    public void run() {
        for (int i = 0; i < 1000; i++) {
            counter.increment();
        }
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        
        MyThread t1 = new MyThread(counter);
        MyThread t2 = new MyThread(counter);
        
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();
        
        System.out.println("Count: " + counter.count);
    }
}
Output:
Count: 1823  (Expected: 2000, but result varies each time!)

Explanation:

  • Race Condition: Both threads try to increment the counter simultaneously
  • Data Loss: When two threads read the same value before either writes back, one increment is lost
  • Unpredictable Result: The final count is different every time you run the program

Types of Synchronization

1. Synchronized Method

When you declare a method as synchronized, only one thread can execute it at a time. Other threads must wait until the current thread finishes execution.

Syntax: synchronized returnType methodName() { }

2. Synchronized Block

Instead of synchronizing the entire method, you can synchronize only a specific block of code. This provides better performance when only part of the method needs synchronization.

Syntax: synchronized(object) { // critical section }

3. Static Synchronization

Used to synchronize static methods. It locks the class instead of the object, ensuring that only one thread can access any static synchronized method of the class at a time.

Syntax: synchronized static returnType methodName() { }

Solution With Synchronized Method

Now let's fix the race condition using a synchronized method:

Example: With Synchronized Method

class Counter {
    int count = 0;
    
    // Synchronized method
    public synchronized void increment() {
        count++;
    }
}

class MyThread extends Thread {
    Counter counter;
    
    MyThread(Counter counter) {
        this.counter = counter;
    }
    
    public void run() {
        for (int i = 0; i < 1000; i++) {
            counter.increment();
        }
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        
        MyThread t1 = new MyThread(counter);
        MyThread t2 = new MyThread(counter);
        
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();
        
        System.out.println("Count: " + counter.count);
    }
}
Output:
Count: 2000  (Correct result every time!)

Explanation:

  • synchronized keyword: Only one thread can execute the increment() method at a time
  • Thread Safety: Other threads wait until the current thread completes the method
  • Consistent Result: Now we get the expected count of 2000 every time

Synchronized Block Example

Synchronized blocks allow you to synchronize only the critical section of code:

Example: Using Synchronized Block

class BankAccount {
    private int balance = 1000;
    
    public void withdraw(int amount) {
        System.out.println(Thread.currentThread().getName() + " attempting to withdraw " + amount);
        
        // Only synchronize the critical section
        synchronized(this) {
            if (balance >= amount) {
                System.out.println(Thread.currentThread().getName() + " withdrawing " + amount);
                balance -= amount;
                System.out.println(Thread.currentThread().getName() + " completed. Balance: " + balance);
            } else {
                System.out.println(Thread.currentThread().getName() + " insufficient funds!");
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();
        
        Thread t1 = new Thread(() -> account.withdraw(700), "Person-1");
        Thread t2 = new Thread(() -> account.withdraw(500), "Person-2");
        
        t1.start();
        t2.start();
    }
}
Output:
Person-1 attempting to withdraw 700
Person-1 withdrawing 700
Person-1 completed. Balance: 300
Person-2 attempting to withdraw 500
Person-2 insufficient funds!

Explanation:

  • synchronized(this): Locks the current object for the block of code
  • Better Performance: Only the critical section is locked, not the entire method
  • Thread Safety: Prevents both threads from withdrawing simultaneously and overdrawing the account

Key Points to Remember

  • Synchronization prevents multiple threads from accessing shared resources simultaneously
  • Use synchronized methods when the entire method needs protection
  • Use synchronized blocks for better performance when only part of the method is critical
  • Synchronization can cause performance overhead - use it only when necessary
  • Over-synchronization can lead to deadlock - be careful with multiple locks

💡 Best Practice: Always synchronize the smallest possible section of code. This minimizes the performance impact while ensuring thread safety.