0
Explore
0

Synchronization in Java – A Complete Tutorial

Updated on August 1, 2025

In a multithreaded environment, multiple threads can access shared resources (like variables, objects, or files) at the same time. This can lead to data inconsistency, unexpected results, or even program crashes.

To prevent such problems, Java provides a powerful mechanism called Synchronization.

What is Synchronization?

Synchronization is the process of controlling access to shared resources by multiple threads in a multithreaded environment. It ensures that only one thread can access the critical section (a block of code that modifies shared data) at a time.

Think of it like a one-person-at-a-time restroom, only one person (thread) can enter at a time to avoid clashes.

Why is Synchronization Needed?

Let’s understand with an example.

Example Without Synchronization:

class Counter {
    int count = 0;

    void increment() {
        count++; // Critical Section
    }
}

public class TestWithoutSync extends Thread {
    Counter counter;

    TestWithoutSync(Counter counter) {
        this.counter = counter;
    }

    public void run() {
        for (int i = 0; i < 1000; i++) {
            counter.increment();
        }
    }

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

        TestWithoutSync t1 = new TestWithoutSync(counter);
        TestWithoutSync t2 = new TestWithoutSync(counter);

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

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

        System.out.println("Final Count: " + counter.count);
    }
}

Expected Output:

Final Count: 2000

Actual Output:

Final Count: 1876 (or some random value)

❓ Why This Happens?

The count++ operation is not atomic. It actually involves 3 steps:

  1. Read the current value of count
  2. Increment it by 1
  3. Write the new value back

Now imagine two threads doing this at the same time — they might both read the same value and overwrite each other’s result.

This is called a race condition — the output depends on the unpredictable “race” between threads.

Real-World Example (Bank ATM Withdrawal)

Let’s say two people are trying to withdraw money from the same bank account at the same time from different ATMs.

class BankAccount {
    int balance = 1000;

    void withdraw(int amount) {
        if (balance >= amount) {
            System.out.println(Thread.currentThread().getName() + " is withdrawing " + amount);
            balance = balance - amount;
            System.out.println(Thread.currentThread().getName() + " has completed withdrawal.");
        } else {
            System.out.println(Thread.currentThread().getName() + " - Insufficient balance");
        }
    }
}

public class BankTest extends Thread {
    BankAccount account;

    BankTest(BankAccount account) {
        this.account = account;
    }

    public void run() {
        account.withdraw(700);
    }

    public static void main(String[] args) {
        BankAccount account = new BankAccount();

        BankTest user1 = new BankTest(account);
        BankTest user2 = new BankTest(account);

        user1.setName("User1");
        user2.setName("User2");

        user1.start();
        user2.start();
    }
}

Output Without Synchronization:

Both users might end up withdrawing 700 each, resulting in negative balance, which is logically incorrect.

Solution: Use Synchronization

Let’s fix our first example using synchronized:

class Counter {
    int count = 0;

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

Now, when one thread is executing increment(), the other thread has to wait. This ensures thread safety.

Output After Synchronization:

Now the final count will always be:

Final Count: 2000

Key Takeaways

  • count++ is not a thread-safe operation.
  • Without synchronization, multiple threads can cause data inconsistency.
  • Use synchronized keyword to protect critical sections.
  • Synchronization ensures only one thread accesses shared resource at a time.

The synchronized Keyword

Java provides the synchronized keyword to lock a method or block so that only one thread can access it at a time.

There are two main ways to use it:

1. Synchronized Method

When you mark a method as synchronized, the entire method becomes a critical section.

Syntax of Synchronized Method

synchronized void myMethod() {
    // Critical section
}

This locks the entire method for one thread at a time.

Java Program

class Printer {
    synchronized void printDocument(String doc) {
        System.out.println(Thread.currentThread().getName() + " is printing " + doc);
        try {
            Thread.sleep(1000); // Simulate print time
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " finished printing " + doc);
    }
}
public class SyncMethodExample extends Thread {
    Printer printer;
    String doc;

    SyncMethodExample(Printer printer, String doc) {
        this.printer = printer;
        this.doc = doc;
    }

    public void run() {
        printer.printDocument(doc);
    }

    public static void main(String[] args) {
        Printer printer = new Printer();

        SyncMethodExample t1 = new SyncMethodExample(printer, "Resume.pdf");
        SyncMethodExample t2 = new SyncMethodExample(printer, "Invoice.docx");

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

Output:

Thread-0 is printing Resume.pdf
Thread-0 finished printing Resume.pdf
Thread-1 is printing Invoice.docx
Thread-1 finished printing Invoice.docx

Only one thread prints at a time — the entire method is locked.

Behind the Scenes:

  • For non-static synchronized methods, the lock is acquired on the current object (this).
  • For static synchronized methods, the lock is acquired on the class object (ClassName.class).

2. Synchronized Block

Synchronized blocks let you lock only a portion of your method — offering more fine-grained control and better performance.

Syntax of Synchronized Block

void myMethod() {
    synchronized(this) {
        // Critical section
    }
}

Java Program

class Printer {
    void printDocument(String doc) {
        System.out.println(Thread.currentThread().getName() + " is preparing to print " + doc);

        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + " is printing " + doc);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " finished printing " + doc);
        }

        System.out.println(Thread.currentThread().getName() + " is doing post-print work...");
    }
}

public class SyncMethodExample extends Thread {
    Printer printer;
    String doc;

    SyncMethodExample(Printer printer, String doc) {
        this.printer = printer;
        this.doc = doc;
    }

    public void run() {
        printer.printDocument(doc);
    }

    public static void main(String[] args) {
        Printer printer = new Printer();

        SyncMethodExample t1 = new SyncMethodExample(printer, "Resume.pdf");
        SyncMethodExample t2 = new SyncMethodExample(printer, "Invoice.docx");

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

This locks only a specific block of code, giving you more control and better performance.

Synchronized on Custom Objects

You can also synchronize using custom lock objects instead of this:

public class SharedPrinter {
    private final Object lock = new Object();

    void print(String doc) {
        synchronized (lock) {
            System.out.println("Printing: " + doc);
        }
    }
}

Using custom lock objects can help you:

  • Synchronize specific portions of a class
  • Avoid blocking unrelated operations
  • Separate multiple locks within the same class

How Synchronization Works Internally(In short)

Every Java object has a monitor lock (aka intrinsic lock). When a thread enters a synchronized block or method, it acquires the lock. Other threads trying to access the same block will wait until the lock is released.

Synchronized Method vs Synchronized Block

FeatureSynchronized MethodSynchronized Block
Lock ScopeWhole methodSpecific block of code
PerformanceSlower (locks everything)Faster (locks only needed part)
FlexibilityLess flexibleMore flexible

Static Synchronization in Java

We know that the synchronized keyword ensures only one thread at a time can access a synchronized method or block. But did you know that static methods can also be synchronized?

What Happens in Static Synchronized Methods?

When you declare a static method as synchronized, Java locks the Class object, not the instance of the class.

Static methods can also be synchronized.

class Counter {
    static int count = 0;

    public static synchronized void increment() {
        count++;
    }
}

⚠️ Here, the class-level lock is acquired, not the object-level lock.

Key Point:

  • For non-static synchronized methods → lock is on the object (this)
  • For static synchronized methods → lock is on the class object (Counter.class)

Why Is That Important?

Because all instances of the class share the same static method and variable, we must lock at the class level to prevent race conditions across all threads, regardless of the object they belong to.

Reentrant Synchronization

Now let’s talk about Reentrant Synchronization, a feature that makes Java’s synchronized blocks and methods more flexible and powerful. Java uses Reentrant Locks. This means a thread can re-enter the same lock if it already holds it.

What is Reentrancy?

Java locks are reentrant, meaning a thread that already holds a lock can acquire it again without blocking itself.

In simple words:

If you already have the key to the door, you can go through another door that requires the same key — no one stops you.

Example:

class Demo {
    synchronized void method1() {
        System.out.println("Inside method1");
        method2();  // Same thread enters another synchronized method
    }

    synchronized void method2() {
        System.out.println("Inside method2");
    }
}

No deadlock occurs because the thread is allowed to re-enter.

What’s Happening?

  1. A thread calls method1() and acquires the object-level lock.
  2. Inside method1(), it calls method2() — which is also synchronized.
  3. Because the same thread already owns the lock, it is allowed to enter method2() without waiting.
  4. No deadlock occurs.

Why Is Reentrancy Important?

Without reentrant locks:

  • A thread could block itself, waiting for a lock it already holds, causing a deadlock!

This design allows for modular and nested synchronized methods, which is common in real-world applications.

Best Practices for Synchronization

✅ Only synchronize critical sections (not entire methods if unnecessary)
✅ Avoid nested synchronization (can lead to deadlocks)
✅ Prefer using concurrent collections like ConcurrentHashMap
✅ Use volatile for visibility when full synchronization isn’t required
✅ Consider using higher-level constructs like AtomicInteger, Semaphore, or ReentrantLock

Common Interview Questions

1. What is synchronization in Java?

Synchronization is a mechanism that ensures only one thread can access a shared resource (like a variable or method) at a time to prevent data inconsistency and race conditions.

2. Difference between synchronized method and block?

Synchronized MethodSynchronized Block
Locks the entire methodLocks a specific section of code
Less controlMore control and performance
Automatically uses this as lockCan lock on any object

3. Can we synchronize static methods?

Yes. When a static method is synchronized, the lock is acquired on the Class object (e.g., ClassName.class), not on any instance.

4. What is a monitor lock?

A monitor lock (or intrinsic lock) is a lock associated with every Java object. It is used by threads to gain exclusive access to synchronized methods or blocks.

5. What is reentrant synchronization?

Reentrant synchronization means a thread can re-acquire the same lock it already holds. This allows nested synchronized calls without blocking itself.

6. Difference between synchronized and ReentrantLock?

synchronizedReentrantLock
Keyword-based, simplerClass-based, more flexible
Automatically releases lockMust manually lock() and unlock()
No try-lock or timeout supportSupports try-lock, timeout, fairness
Less controlMore advanced features

Conclusion

Synchronization is one of the most important concepts in Java multithreading. It helps prevent race conditions, ensures data consistency, and keeps your applications safe and reliable.