Synchronization in Java

14 Apr 2026, Updated: 12 Jul 2026 6 min read
2
When multiple threads access shared data, race conditions can occur, leading to inconsistent and incorrect results.

To prevent this, Java provides a mechanism called synchronization, which ensures that only one thread can access a critical section at a time.

Synchronization is primarily achieved using the synchronized keyword, which relies on an internal locking mechanism known as intrinsic locks or monitor locks.

The "synchronized" Keyword

The synchronized keyword is used to control access to a block of code or method so that only one thread can execute it at a time for the same object.

When a thread enters a synchronized section, it acquires a lock. Other threads attempting to enter the same section must wait until the lock is released.
class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}
In this example, only one thread can execute the increment() method at a time for the same Counter object.

How synchronized Works?

Every object in Java has an associated intrinsic (monitor) lock. When a thread enters a synchronized method or block, it attempts to acquire that lock.

1. If the lock is available, the thread acquires it immediately and continues execution.

2. If another thread already holds the lock, the current thread enters the BLOCKED state until the lock becomes available.

3. When the synchronized method or block finishes, the lock is released automatically.

Synchronization only works when competing threads synchronize on the same lock object.
class Counter {
    private int count = 0;
    private final Object lock = new Object();

    // Uses the object's intrinsic lock
    public void incrementUsingThis() {
        synchronized (this) {
            count++;
        }
    }

    // Uses a different lock object
    public void incrementUsingLock() {
        synchronized (lock) {
            count++;
        }
    }
}
If one thread executes incrementUsingThis() while another executes incrementUsingLock(), both methods can run at the same time because they synchronize on different lock objects (this and lock).

Since the shared variable count is being modified concurrently, synchronization does not provide the intended protection, and race conditions can still occur.

Method vs Block-Level Synchronization

Method-Level Synchronization

When you declare a method as synchronized, the lock is acquired before entering the method and released when the method exits.
class Counter {
    private int count = 0;

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

Characteristics:

- Locks the entire method
- Simple and easy to use
- May reduce performance if overused

Block-Level Synchronization

Instead of locking the entire method, you can synchronize only a specific block of code. This provides more control and better performance.
class Counter {
    private int count = 0;

    public void increment() {
        synchronized (this) {
            count++;
        }
    }
}
Here, only the critical section is synchronized, allowing the remaining code to execute without holding the lock.

The object specified inside the synchronized(...) statement determines which lock is acquired. Different lock objects allow different synchronized sections to execute concurrently.

Advantages:

- Better performance (smaller locked region)
- More fine-grained control
- Allows different locks for different resources

Comparison

Method-Level Synchronization Block-Level Synchronization
Locks the entire method. Locks only the specific block of code.
Easier to implement. More efficient.
Less flexible. Preferred in real-world applications.
In practice, block-level synchronization is generally preferred for better performance and scalability.

Intrinsic Locks (Monitor Locks)

Java uses intrinsic locks, also known as monitor locks, to implement synchronization.

Every object has a built-in lock that can be used for synchronization. When a thread enters a synchronized block or method, it acquires the object's monitor lock.
Object lock = new Object();

synchronized (lock) {
    // Thread holds the object's monitor lock
    System.out.println("Inside synchronized block");
}
Intrinsic locks are invoked automatically behind the scenes when you use the synchronized keyword. They come in two forms:

1. Instance Locks (Object-Level)

Used when a synchronized block locks the current instance (synchronized(this)) or when a non-static method is marked synchronized.

A. Synchronized Method

The lock is implicitly the this instance.
public class InstanceMethodCounter {
    private int count = 0;

    // Locks the specific object instance ('this')
    public synchronized void increment() {
        count++;
    }

    public int getCount() { return count; }
}

B. Synchronized Block with this

Explicitly locks the this instance. Useful for locking only a specific portion of a method.
public class InstanceBlockCounter {
    private int count = 0;

    public void increment() {
        // Non-thread-safe preparation code can go here unblocked
        synchronized (this) {
            count++; // Locked on the 'this' instance
        }
    }
}

C. Private Object Lock (Best Practice)

Locks a dedicated, hidden internal instance variable. Completely prevents external code from hijacking your lock.
public class PrivateInstanceLockCounter {
    private int count = 0;
    // Every object instance gets its own private lock object
    private final Object lock = new Object(); 

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

2. Static Locks

These locks protect static (global) variables. Only one thread can hold the lock for that class at a time, regardless of how many instances of the class exist.

Used when synchronizing on the Class object itself (e.g., within a static synchronized method).

A. Static Synchronized Method

The lock is implicitly the Counter.class object.
public class StaticMethodCounter {
    private static int globalCount = 0;

    // Locks the entire StaticMethodCounter.class
    public static synchronized void incrementGlobal() {
        globalCount++;
    }

    public static int getGlobalCount() { return globalCount; }
}

B. Synchronized Block with .class Literal

Explicitly locks the Class metadata object. Often used inside non-static methods to modify global data safely.
public class StaticClassBlockCounter {
    private static int globalCount = 0;

    public void incrementGlobal() {
        // Locks the Class object across the entire JVM
        synchronized (StaticClassBlockCounter.class) {
            globalCount++;
        }
    }
}

C. Static Private Object Lock (Best Practice for Globals)

A hidden, class-level lock object. Safeguards global resources without exposing the .class lock publicly.
public class PrivateStaticLockCounter {
    private static int globalCount = 0;
    // Shared across ALL instances, hidden from outside code
    private static final Object staticLock = new Object(); 

    public void incrementGlobal() {
        synchronized (staticLock) {
            globalCount++;
        }
    }
}

Key Properties of Intrinsic Locks:

- Each object has one monitor (intrinsic) lock
- Only one thread can hold the lock at a time
- Locks are reentrant (same thread can acquire multiple times)
- Automatically released when exiting synchronized block

Reentrancy Example

Intrinsic locks in Java are reentrant, meaning a thread that already owns a lock can acquire the same lock again without blocking.
class Example {
    public synchronized void methodA() {
        methodB();
    }

    public synchronized void methodB() {
        System.out.println("Reentrant lock acquired");
    }
} 
Here, the same thread can enter methodB() even though it already holds the lock from methodA().

In the next chapter, we will explore the volatile keyword and understand how Java ensures memory visibility between threads, even when no locking is used.
Nagesh Chauhan

Nagesh Chauhan

Principal Software Engineer • Java • Python • Distributed Systems • AI/ML

Principal Software Engineer with 14+ years of experience designing and delivering large-scale distributed systems, cloud-native applications, and AI-powered platforms.

Passionate about solving complex engineering problems using strong data structures and algorithms, along with expertise in Java, Spring Boot, Python, System Design, Microservices, Cloud, Kafka, Elasticsearch, and Generative AI.

Share this Article

💬 Comments

Join the Discussion