Topic 97 of 119 11. Multithreading in Java Interactive Tutorial

Thread Synchronization in Java: Mutual Exclusion and the synchronized Keyword

10 mins read Last updated: 22 Aug 2026 31,781 views

Direct Answer & Core Concept

When multiple threads access the same data at the same time, unexpected results can occur. Two threads may read and modify the same variable simultaneously, causing a race condition.

Java provides synchronization mechanisms to coordinate access to shared resources. One of the most fundamental tools is the synchronized keyword, which provides mutual exclusion around synchronized code.

In this tutorial, you will learn what thread synchronization means, why race conditions happen, how intrinsic locks work, and how to use synchronized methods and synchronized blocks safely.

Detailed Explanation & Mechanics

What Is Thread Synchronization in Java?

Thread synchronization is the process of controlling how multiple threads access shared data or shared resources.

Consider two threads trying to update the same bank account:

Architecture Diagram
Thread A ──┐
           ├──> Shared Account
Thread B ──┘

If both threads modify the account at the same time without proper coordination, one update may overwrite another.

Synchronization helps ensure that critical operations are performed in a controlled way.


What Is Mutual Exclusion?

Mutual exclusion, often called mutex, means that only one thread at a time can enter a particular protected section of code guarded by the same lock.

For example:

Main.java
Run Online
Shared Resource | +-------+-------+ | | Thread A Thread B | | +-------+-------+ | One at a time

If Thread A owns the lock, Thread B must wait before entering the same synchronized region protected by that lock.

This prevents multiple threads from simultaneously executing conflicting operations on the same protected state.


Why Is Synchronization Necessary?

Suppose we have a counter:

Main.java
Run Online
class Counter {
    int count = 0;
    void increment() {
        count++;
    }
}

It may look like count++ is one simple operation.

Conceptually, however, it involves multiple steps:

Main.java
Run Online
Read count↓Add 1↓Write count

If two threads perform these steps concurrently, their operations can overlap.

For example:

Main.java
Run Online
Thread A: Read 10Thread B: Read 10Thread A: Write 11Thread B: Write 11

The expected result after two increments is 12, but the final value can be 11.

This is a classic lost update caused by a race condition.


What Is a Race Condition?

A race condition occurs when the result of a program depends on the timing or interleaving of concurrent operations.

For example:

Main.java
Run Online
class Counter {
    private int count = 0;
    public void increment() {
        count++;
    } public int getCount() {
        return count;
    }
}

Now imagine multiple threads repeatedly calling:

Without appropriate synchronization, the final value may be lower than expected.

The problem becomes especially important when an operation involves a read-modify-write sequence.


The synchronized Keyword

Java provides the synchronized keyword to protect critical sections.

It can be used with:

  • instance methods

  • static methods

  • code blocks

For example:

Main.java
Run Online
class Counter {
    private int count = 0;
    public synchronized void increment() {
        count++;
    } public synchronized int getCount() {
        return count;
    }
}

When a thread enters a synchronized instance method, it acquires the object's intrinsic lock associated with that method.

Another thread attempting to enter a synchronized instance method protected by the same object lock must wait until the lock is released.


Synchronized Instance Method

A synchronized instance method looks like this:

Main.java
Run Online
class Counter {
    private int count;
    public synchronized void increment() {
        count++;
    }
}

The important part is:

placed before the return type.

For an instance method, the synchronization is associated with the particular object instance.

Conceptually, this:

Main.java
Run Online
public synchronized void increment() {
    count++;
}

is similar in locking behavior to:

Main.java
Run Online
public void increment() {
    synchronized (this) {
        count++;
    }
}

The two forms synchronize on the same object, although the explicit block can make the protected region more precise.


Example With Multiple Threads

Let's create a counter shared by multiple threads.

Main.java
Run Online
class Counter {
    private int count = 0;
    public synchronized void increment() {
        count++;
    } public int getCount() {
        return count;
    }
}public class Main {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0;
            i < 1000;
            i++) {
                counter.increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0;
            i < 1000;
            i++) {
                counter.increment();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.getCount());
    }
}

The expected result is:

The synchronized method prevents the two threads from simultaneously executing the protected increment() operation on the same Counter object.


What Is an Intrinsic Lock?

Every Java object has an associated intrinsic lock, also called a monitor lock.

When a thread enters a synchronized instance method or a synchronized block using that object as the monitor, it acquires that object's lock.

For example:

Main.java
Run Online
synchronized (counter) {
    // protected code
}

The counter object acts as the lock.

Only one thread at a time can hold that particular lock.


How the Lock Works

Imagine two threads:

Main.java
Run Online
Thread A | | acquire lock↓[ Critical Section] | | release lock↓Thread B | | acquire lock↓[ Critical Section]

If Thread B tries to enter the same synchronized region while Thread A owns the lock, Thread B cannot enter until the lock becomes available.

This is the basic mechanism behind mutual exclusion.


Synchronized Block

Instead of synchronizing an entire method, you can synchronize only the critical section.

Main.java
Run Online
class Counter {
    private int count = 0;
    public void increment() {
        synchronized (this) {
            count++;
        }
    }
}

The object used inside the parentheses is the monitor.

Here:

means the current object is used as the lock.


Why Use a Synchronized Block?

A synchronized method may protect more code than necessary.

For example:

Main.java
Run Online
public synchronized void process() {
    performStepOne();
    performStepTwo();
    count++;
    performStepThree();
}

If only count++ needs protection, locking the entire method may unnecessarily reduce concurrency.

A narrower block can be better:

Main.java
Run Online
public void process() {
    performStepOne();
    performStepTwo();
    synchronized (this) {
        count++;
    } performStepThree();
}

The general principle is:

Protect the shared state that requires coordination, but avoid holding a lock longer than necessary.


Using a Dedicated Lock Object

You do not always need to synchronize on this.

You can create a private lock object:

Main.java
Run Online
class Counter {
    private int count = 0;
    private final Object lock = new Object();
    public void increment() {
        synchronized (lock) {
            count++;
        }
    }
}

This has an important advantage: external code cannot easily obtain the private lock object because it is not exposed.

A private lock can therefore provide better encapsulation.


Why Synchronizing on this Can Be Risky

Consider:

Main.java
Run Online
public synchronized void update() {
    // ...
}

This uses this as the monitor.

External code can also synchronize on the same object:

Main.java
Run Online
synchronized (someObject) {
    // ...
}

If someObject is the same instance, unrelated code can participate in the same locking protocol.

Using a private lock:

Main.java
Run Online
private final Object lock = new Object();

keeps the synchronization mechanism internal to the class.


Static Synchronized Methods

The behavior is different for static synchronized methods.

Example:

Main.java
Run Online
class SharedCounter {
    private static int count = 0;
    public static synchronized void increment() {
        count++;
    }
}

A static synchronized method locks on the Class object, not on an individual instance.

Conceptually:

acts as the monitor.

Therefore, this:

Main.java
Run Online
public static synchronized void increment()

is conceptually similar to:

Main.java
Run Online
public static void increment() {
    synchronized (SharedCounter.class) {
        count++;
    }
}

Instance Lock vs Class Lock

This distinction is extremely important.

Instance synchronized method

Main.java
Run Online
public synchronized void method() {
}

Uses:

as the monitor.

Static synchronized method

Main.java
Run Online
public static synchronized void method() {
}

Uses:

as the monitor.

They are different locks.

Therefore, synchronizing an instance method does not automatically prevent another thread from executing a static synchronized method.


Example of Different Locks

Main.java
Run Online
class Example {
    public synchronized void instanceMethod() {
        System.out.println("Instance lock");
    } public static synchronized void staticMethod() {
        System.out.println("Class lock");
    }
}

The first method locks the particular Example object.

The second method locks:

These are separate synchronization mechanisms.


Synchronized Blocks With Different Locks

Consider:

Main.java
Run Online
synchronized (lockA) {
    // protected section
}

and:

Main.java
Run Online
synchronized (lockB) {
    // protected section
}

If lockA and lockB are different objects, they represent different locks.

Two threads can potentially execute these blocks simultaneously.

Main.java
Run Online
lockA → Thread AlockB → Thread BBoth can execute concurrently

This is why choosing the correct lock is important.


Synchronization Protects a Critical Section

A critical section is code that accesses shared mutable state and must be coordinated among threads.

For example:

Main.java
Run Online
synchronized (lock) {
    balance = balance - amount;
}

The operation that modifies the shared balance is protected by the lock.

The goal is not to synchronize every line of code. The goal is to protect operations that require coordination.


Synchronization and Visibility

Synchronization is not only about mutual exclusion.

It also provides important memory visibility guarantees between threads.

When one thread releases an intrinsic lock and another thread subsequently acquires the same lock, changes made before the release become visible according to Java's memory model.

For example:

Main.java
Run Online
synchronized (lock) {
    value = 100;
}

and later:

Main.java
Run Online
synchronized (lock) {
    System.out.println(value);
}

Using the same lock establishes the required synchronization relationship.

This is one reason synchronization is more than simply preventing two threads from entering code at the same time.


What Happens When a Thread Cannot Get the Lock?

Suppose Thread A owns a lock:

Thread B attempts to enter a synchronized region using the same monitor:

Main.java
Run Online
Thread B → waiting for monitor

Thread B cannot execute that synchronized region until Thread A releases the lock.

Once the lock becomes available, a waiting thread may acquire it.

The JVM and operating system determine scheduling details, so you should not assume a particular waiting thread will always acquire the lock first.


Synchronized Does Not Make Everything Thread-Safe

Adding synchronized to one method does not automatically make an entire class thread-safe.

For example:

Main.java
Run Online
class Counter {
    private int count;
    public synchronized void increment() {
        count++;
    } public int getCount() {
        return count;
    }
}

The increment operation is synchronized, but thread safety depends on how the complete state and all accesses to it are designed.

A thread-safe design requires consistent coordination around shared mutable state.


Common Synchronization Mistake

Consider:

Main.java
Run Online
class Counter {
    private int count;
    public synchronized void increment() {
        count++;
    } public synchronized void decrement() {
        count--;
    }
}

Both methods use the same instance lock, so they cannot execute simultaneously on the same object.

This is different from:

Main.java
Run Online
class Counter {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    private int count;
    public void increment() {
        synchronized (lock1) {
            count++;
        }
    } public void decrement() {
        synchronized (lock2) {
            count--;
        }
    }
}

Here, two different locks protect the same variable.

That design does not provide the same mutual exclusion between increment() and decrement().

The choice of lock must match the state being protected.


Reentrant Synchronization

Java's intrinsic locks are reentrant.

This means a thread that already owns a monitor can acquire the same monitor again.

For example:

Main.java
Run Online
class Example {
    public synchronized void methodA() {
        methodB();
    } public synchronized void methodB() {
        System.out.println("Inside method B");
    }
}

When methodA() calls methodB() on the same object, the same thread can acquire the object's monitor again.

The thread does not deadlock itself simply because it enters another synchronized method using the same monitor.


Synchronization and Deadlocks

Poor lock design can cause a deadlock.

For example:

Main.java
Run Online
Thread A:Lock A↓Wait for Lock BThread B:Lock B↓Wait for Lock A

Neither thread can continue.

The problem can be visualized as:

Architecture Diagram
Thread A → Lock A → waiting for Lock B
                         ↑
                         |
Thread B → Lock B ───────┘

Both threads wait indefinitely.

To reduce deadlock risk:

  • Keep locking order consistent.

  • Avoid unnecessary nested locks.

  • Keep synchronized regions small.

  • Avoid calling unknown external code while holding locks.

  • Carefully document lock ownership when complex synchronization is required.


Example: Bank Account

Synchronization is commonly demonstrated with shared bank account data.

Main.java
Run Online
class BankAccount {
    private double balance;
    public BankAccount(double balance) {
        this.balance = balance;
    } public synchronized void withdraw(double amount) {
        if (balance >= amount) {
            balance -= amount;
            System.out.println( "Withdrawn: " + amount);
        } else {
            System.out.println( "Insufficient balance");
        }
    } public synchronized double getBalance() {
        return balance;
    }
}

The check and update are performed inside the same synchronized method.

That matters because this sequence:

Main.java
Run Online
Check balance↓Subtract amount↓Update balance

needs to be treated as one coordinated operation.


Why Check-Then-Act Operations Need Care

Consider:

Main.java
Run Online
if (balance >= amount) {
    balance -= amount;
}

The condition and update form one logical operation.

If multiple threads can execute them without synchronization, both threads could observe the same balance before either performs the withdrawal.

Synchronization can protect the complete check-and-update sequence.

This is an important concurrency principle:

Protect the entire operation whose correctness depends on shared state remaining consistent.


Synchronized Method vs Synchronized Block

Synchronized method

Main.java
Run Online
public synchronized void update() {
    count++;
}

Advantages:

  • Simple syntax

  • Easy to understand

  • Entire method is protected

Synchronized block

Main.java
Run Online
public void update() {
    // Other work
    synchronized (lock) {
        count++;
    } // More work
}

Advantages:

  • More precise locking

  • Smaller critical section

  • Can use a dedicated lock object

Use the form that makes the synchronization boundary clear and correct.


Performance Considerations

Synchronization has overhead, but correctness should come before attempting to optimize away necessary synchronization.

A common mistake is to remove synchronization simply because it may affect performance.

Instead:

Main.java
Run Online
Correct synchronization↓Measure performance↓Identify actual bottlenecks↓Optimize where justified

Keeping critical sections reasonably small can allow more concurrency, but making synchronization unnecessarily complicated can introduce correctness problems.


synchronized vs volatile

These two keywords solve different problems.

volatile

A volatile variable provides visibility and certain ordering guarantees for reads and writes of that variable.

It does not make compound operations such as:

atomic.

synchronized

Synchronization provides mutual exclusion and memory synchronization around the protected region.

For example:

Main.java
Run Online
synchronized (lock) {
    count++;
}

This protects the complete operation from concurrent execution by another thread using the same lock.


synchronized vs Atomic Classes

For simple counters, Java also provides atomic classes such as:

Example:

Main.java
Run Online
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();

Atomic classes can be useful for specific state-update patterns without using an intrinsic monitor.

However, they are not a universal replacement for synchronization. More complicated invariants involving multiple pieces of shared state may still require locks or other coordination mechanisms.


Best Practices for synchronized

Follow these principles when using synchronization:

1. Protect shared mutable state

Identify which state can be accessed concurrently.

2. Use a consistent lock

All operations that need mutual exclusion must coordinate through the same lock.

3. Keep critical sections focused

Avoid holding a lock while performing slow or unrelated operations.

4. Prefer private lock objects when appropriate

For internal synchronization, a private final lock can improve encapsulation.

Main.java
Run Online
private final Object lock = new Object();

5. Avoid unnecessary nested locks

Multiple locks increase complexity and can increase deadlock risk.

6. Do not assume thread execution order

Synchronization provides coordination; it does not mean threads execute in a predetermined order.


Common Mistakes

Mistake 1: Synchronizing Different Locks

Main.java
Run Online
synchronized (lockA) {
    count++;
}

and:

Main.java
Run Online
synchronized (lockB) {
    count--;
}

If both protect the same shared state, they do not provide mutual exclusion because they use different monitors.


Mistake 2: Synchronizing Only Part of a Compound Operation

Incorrectly protecting only the final assignment can still leave the read-modify-write sequence vulnerable.

The complete logical operation should be protected.


Mistake 3: Calling Slow Operations While Holding a Lock

For example:

Main.java
Run Online
synchronized (lock) {
    callSlowNetworkOperation();
}

Holding a lock during a slow external operation can unnecessarily prevent other threads from accessing the protected resource.


Mistake 4: Assuming synchronized Means "Only One Thread in the Whole Program"

It does not.

Synchronization is associated with a particular monitor.

Two threads can execute synchronized sections concurrently if those sections use different locks.


Quick Example to Remember

Without synchronization:

Main.java
Run Online
class Counter {
    private int count = 0;
    void increment() {
        count++;
    }
}

With synchronization:

Main.java
Run Online
class Counter {
    private int count = 0;
    synchronized void increment() {
        count++;
    }
}

Or:

Main.java
Run Online
class Counter {
    private int count = 0;
    void increment() {
        synchronized (this) {
            count++;
        }
    }
}

The second and third versions use the object's intrinsic monitor to coordinate access.


Frequently Asked Questions

What is synchronization in Java?

Synchronization is a mechanism for coordinating access to shared state among threads. It can provide mutual exclusion and memory-visibility guarantees.

What does synchronized do?

It causes a thread to acquire a monitor before entering the synchronized method or block and release that monitor when it leaves the synchronized region.

What is mutual exclusion?

Mutual exclusion means that only one thread at a time can execute a protected region guarded by the same lock.

What is an intrinsic lock?

An intrinsic lock, also called a monitor lock, is the lock associated with every Java object and used by synchronized.

What lock does a synchronized instance method use?

A synchronized instance method uses the monitor associated with the particular object instance.

What lock does a static synchronized method use?

A static synchronized method uses the monitor associated with the class object.

Can synchronized methods cause deadlocks?

Yes. Multiple locks acquired in inconsistent orders can result in deadlock.

Is count++ thread-safe?

Not merely because count is a normal variable. count++ is a compound read-modify-write operation and requires appropriate coordination when multiple threads update the same variable.

Does synchronized guarantee fairness?

No. The Java synchronized mechanism does not provide a general fairness guarantee about which waiting thread obtains a monitor next.

Does synchronization improve performance?

Its primary purpose is correctness, not performance. Synchronization introduces coordination overhead, so critical sections should generally be kept focused without sacrificing correctness.

Suggested Graphic

Create a simple diagram showing two threads competing for one monitor:

Architecture Diagram
             Shared Resource
                   |
              [ LOCK ]
                   |
          +--------+--------+
          |                 |
       Thread A           Thread B
          |                 |
      acquire lock       waits
          |
     Critical Section
          |
      release lock
          |
          +---------> Thread B
                       acquires lock
                           |
                     Critical Section

The graphic should visually emphasize the core concept:

One shared lock → one thread at a time → protected critical section.

Runnable Code Example: Thread Synchronization in Java

Java 17+
Main.java
Run Online
public class CounterSyncDemo {
    private int count = 0;
    private final Object lock = new Object();
    public void increment() {
        synchronized (lock) {
            // Fine-grained monitor lock
            count++;
        }
    } public int getCount() {
        synchronized (lock) {
            return count;
        }
    } public static void main(String[] args) throws InterruptedException {
        CounterSyncDemo demo = new CounterSyncDemo();
        Thread t1 = new Thread(() -> {
            for (int i=0;
            i<1000;
            i++) demo.increment();
        });
        Thread t2 = new Thread(() -> {
            for (int i=0;
            i<1000;
            i++) demo.increment();
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Final Synchronized Count: " + demo.getCount());
        // 2000
    }
}

Common Java Technical Interview Questions & Answers

Interview Prep
Q1

What is a race condition in Java?

A concurrency flaw where multiple threads access shared mutable data concurrently and the final result depends on non-deterministic thread scheduling timing.
Q2

What is the difference between volatile and synchronized?

volatile guarantees visibility across CPU caches without mutual exclusion; synchronized guarantees both mutual exclusion (atomicity) and visibility.

Key Takeaways, Best Practices & Clean Code Rules

Java Best Practice & Production Rules

Keep synchronized blocks as small as possible to minimize thread contention and maximize parallel throughput.