← Back to Home

Thread Synchronization

Thread synchronization in Java is the mechanism used to control access to shared resources in a multi-threaded environment. It ensures data consistency, thread safety, and predictable behavior when multiple threads execute concurrently. This is a very high-frequency interview topic.

Why Thread Synchronization Is Needed

In multithreading, multiple threads may access and modify shared data simultaneously, leading to:

  • Race conditions
  • Inconsistent results
  • Data corruption

Example Problem (Race Condition)

class Counter {
    int count = 0;
    void increment() {
        count++;
    }
}
          

Multiple threads calling increment() → incorrect count

What Is Synchronization?

  • Ensures only one thread accesses a critical section at a time
  • Achieved using locks (monitors)
  • Prevents race conditions

Synchronization in Java (Ways)

  1. Synchronized Method
  2. Synchronized Block
  3. Static Synchronization
  4. Inter-thread Communication (wait, notify) (preview)

1️⃣ Synchronized Method

Locks the object (instance).

class Counter {
    int count = 0;
    synchronized void increment() {
        count++;
    }
}
          
  • ✔ Only one thread can execute this method per object
  • ✔ Lock = this

2️⃣ Synchronized Block (Preferred)

Locks specific code, not entire method.

class Counter {
    int count = 0;
    void increment() {
        synchronized (this) {
            count++;
        }
    }
}
          
  • ✔ Better performance
  • ✔ Fine-grained control

3️⃣ Static Synchronization

Locks the class-level object.

class Counter {
    static int count = 0;
    static synchronized void increment() {
        count++;
    }
}
          
  • ✔ Lock = Counter.class
  • ✔ Shared across all objects

Object Lock vs Class Lock (Interview Favorite)

Type Lock Used
Instance method Object lock (this)
Synchronized block Specified object
Static method Class lock (ClassName.class)

What Is a Monitor Lock?

  • Every Java object has an intrinsic lock (monitor)
  • synchronized acquires this lock
  • Released when thread exits synchronized area

Synchronization Example (Correct Output)

class Counter {
    int count = 0;
    synchronized void increment() {
        count++;
    }
}
public class Test {
    public static void main(String[] args) throws Exception {
        Counter c = new Counter();
        Thread t1 = new Thread(() -> {
            for(int i=0;i<1000;i++) c.increment();
        });
        Thread t2 = new Thread(() -> {
            for(int i=0;i<1000;i++) c.increment();
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(c.count); // 2000
    }
}
          

Synchronization and Performance

  • Synchronization adds overhead
  • Excessive synchronization reduces performance
  • Use minimal critical sections

Synchronization Does NOT Solve

  • Deadlocks
  • Thread starvation
  • Livelocks
  • (Handled separately)

Alternatives to synchronized (Preview)

  • java.util.concurrent.locks.Lock
  • ReentrantLock
  • Atomic classes (AtomicInteger)

Common Beginner Mistakes

  • Synchronizing entire methods unnecessarily
  • Synchronizing on wrong object
  • Forgetting static vs instance lock difference
  • Assuming synchronization fixes all concurrency issues

Interview-Ready Answers

Short Answer

Thread synchronization ensures that only one thread accesses shared resources at a time.

Detailed Answer

In Java, thread synchronization is achieved using the synchronized keyword to control access to critical sections of code. It prevents race conditions and ensures data consistency by allowing only one thread to hold a lock on an object or class at a time.

Thread Synchronization Examples (Interview Pack)

1. Race Condition (Without Synchronization)

class Counter {
    int count = 0;

    void increment() {
        count++;
    }

    public static void main(String[] args) throws Exception {
        Counter c = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) c.increment();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) c.increment();
        });

        t1.start(); t2.start();
        t1.join(); t2.join();

        System.out.println(c.count);
    }
}
          

Output: May be less than 2000

Explanation: Demonstrates race condition

2. Synchronized Method (Fixing Race Condition)

class Counter {
    int count = 0;

    synchronized void increment() {
        count++;
    }

    public static void main(String[] args) throws Exception {
        Counter c = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) c.increment();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) c.increment();
        });

        t1.start(); t2.start();
        t1.join(); t2.join();

        System.out.println(c.count);
    }
}
          

Output: 2000

3. Synchronized Block (Better Performance)

class Counter {
    int count = 0;

    void increment() {
        synchronized (this) {
            count++;
        }
    }
}
          

Explanation: Locks only critical section

Why: Better than synchronizing entire method

4. Object-Level Lock (Instance Synchronization)

class Task {
    synchronized void work() {
        System.out.println(Thread.currentThread().getName() + " working");
    }

    public static void main(String[] args) {
        Task obj = new Task();
        new Thread(obj::work).start();
        new Thread(obj::work).start();
    }
}
          

Explanation: Lock is on object instance

5. Two Objects → Two Locks (No Blocking)

class Task {
    synchronized void work() {
        System.out.println(Thread.currentThread().getName() + " working");
    }

    public static void main(String[] args) {
        new Thread(() -> new Task().work()).start();
        new Thread(() -> new Task().work()).start();
    }
}
          

Explanation: Different objects → no synchronization conflict

6. Static Synchronized Method (Class-Level Lock)

class Printer {
    static synchronized void print() {
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        new Thread(Printer::print).start();
        new Thread(Printer::print).start();
    }
}
          

Explanation: Lock is on Class object

7. Object Lock vs Class Lock Difference

class Test {
    synchronized void instanceMethod() {}
    static synchronized void staticMethod() {}
}
          

Explanation: Instance method → object lock

Explanation: Static method → class lock

8. Synchronizing on Custom Lock Object

class Counter {
    int count = 0;
    final Object lock = new Object();

    void increment() {
        synchronized (lock) {
            count++;
        }
    }
}
          

Explanation: Avoids locking this

Why: Better design

9. Synchronization with Runnable

class Task implements Runnable {
    int count = 0;

    public synchronized void run() {
        count++;
    }
}
          

10. Deadlock Example (Very Important)

class Deadlock {
    static final Object A = new Object();
    static final Object B = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                synchronized (B) {
                    System.out.println("Thread 1");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (B) {
                synchronized (A) {
                    System.out.println("Thread 2");
                }
            }
        });

        t1.start();
        t2.start();
    }
}
          

Explanation: Both threads wait forever

Why: Classic deadlock

11. Avoiding Deadlock (Consistent Lock Order)

synchronized (A) {
    synchronized (B) {
        // safe
    }
}
          

Rule: Always acquire locks in same order

12. sleep() Does NOT Release Lock

class Demo {
    synchronized void work() throws Exception {
        Thread.sleep(1000);
    }
}
          

Explanation: Lock held during sleep

13. wait() Releases Lock

class Demo {
    synchronized void work() throws Exception {
        wait();
    }
}
          

Explanation: Lock released until notify()

14. notify() and notifyAll()

class Demo {
    synchronized void resume() {
        notifyAll();
    }
}
          

15. Synchronization Overhead (Concept)

// Over-synchronization reduces performance
          

Best Practice: Synchronize minimum code

16. Thread-Safe Increment Using AtomicInteger

import java.util.concurrent.atomic.AtomicInteger;

class Demo {
    static AtomicInteger count = new AtomicInteger();

    public static void main(String[] args) {
        count.incrementAndGet();
        System.out.println(count.get());
    }
}
          

Explanation: No explicit synchronization

Why: Lock-free

17. Synchronized vs Non-Synchronized Methods

class Demo {
    synchronized void syncMethod() {}
    void normalMethod() {}
}
          

Explanation: Non-sync method runs concurrently

18. Synchronization with Multiple Methods

class Demo {
    synchronized void m1() {}
    synchronized void m2() {}
}
          

Explanation: Same lock → one thread at a time

19. Common Interview Trap

synchronized (new Object()) {
    // useless lock
}
          

Explanation: New object every time → no real synchronization

20. Interview Summary – Thread Synchronization

synchronized
          

Key Points:

  • Prevents race conditions
  • Uses intrinsic locks
  • Object lock vs Class lock
  • Deadlock risk exists
  • Synchronize minimal code

Key Takeaway

Synchronization protects shared data, not threads. Use it carefully and minimally to balance correctness and performance.