Object-Level vs Class-Level Locking in Java

In Java, synchronization can be applied at different levels depending on what resource needs protection. Sometimes each object instance should have its own independent lock, while in other cases all instances of a class must share a common lock. These two approaches are known as object-level locking and class-level locking.

Choosing the correct locking scope is essential for correctness, scalability, and performance. In this article, we will explore how both models work, understand static synchronization, and discuss the important concept of lock granularity.

Object-Level Locking

Object-level locking means the lock belongs to a specific object instance. Each object has its own monitor lock, so threads working with different objects do not block each other.

When a non-static synchronized method is used, Java synchronizes on this (the current object).
class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}
Here, if two threads call increment() on the same Counter object, one must wait. But if they use two different Counter objects, both can run simultaneously.

Example

        Counter c1 = new Counter();
        Counter c2 = new Counter();

        // Different objects = different locks
This improves concurrency because independent objects do not share the same lock.

Class-Level Locking

Class-level locking means the lock belongs to the Class object, not to any specific instance. Since there is only one Class object per loaded class, all instances share the same lock.

This is commonly used when protecting static data shared across every instance.
class Counter {
    private static int total = 0;

    public static synchronized void incrementTotal() {
        total++;
    }
}
Here, the method locks on:
Counter.class
So even if multiple objects exist, only one thread at a time can execute incrementTotal(). When a static method is declared synchronized, Java uses the class-level monitor lock automatically.
class Printer {
    public static synchronized void print() {
        System.out.println("Printing...");
    }
}
Equivalent form using synchronized block:
class Printer {
    public static void print() {
        synchronized (Printer.class) {
            System.out.println("Printing...");
        }
    }
}
Both versions synchronize on the same lock: Printer.class.
An important point is that object locks and class locks are different locks. That means a thread can execute a synchronized instance method while another thread executes a synchronized static method at the same time.
class Demo {
    public synchronized void methodA() { }

    public static synchronized void methodB() { }
}
- methodA() locks object instance
- methodB() locks Demo.class

These do not block each other unless additional logic causes contention.

Lock Granularity

Lock granularity refers to how large or small the locked region is and how broad the lock scope becomes.

Fine-grained locking means smaller, more specific locks. Coarse-grained locking means larger, broader locks covering more code or more resources.

Coarse-Grained Locking

public synchronized void processAll() {
    updateA();
    updateB();
    updateC();
}
One lock protects everything.

Fine-Grained Locking

private final Object lockA = new Object();
private final Object lockB = new Object();

public void updateA() {
    synchronized (lockA) {
        // update A
    }
}

public void updateB() {
    synchronized (lockB) {
        // update B
    }
}
Different resources use different locks.
Synchronizing static data with instance locks is incorrect.
class Wrong {
    private static int total = 0;

    public synchronized void increment() {
        total++; // unsafe across multiple instances
    }
}
Each object has its own lock, but total is shared globally. Different instances can update it concurrently.

Correct solution:
public static synchronized void increment() {
    total++;
}

Conclusion

Object-level locking protects instance-specific data, while class-level locking protects data shared across all instances. Understanding this distinction is critical for correct synchronization design.

Static synchronization uses the class monitor lock, making it ideal for shared static resources. Meanwhile, choosing the right lock granularity balances simplicity with performance.

In the next article, we will explore the volatile keyword and understand how Java guarantees memory visibility between threads.
Nagesh Chauhan
Nagesh Chauhan
Principal Engineer | Java ยท Spring Boot ยท Python ยท Microservices ยท AI/ML

Principal Engineer with 14+ years of experience in designing scalable systems using Java, Spring Boot, and Python. Specialized in microservices architecture, system design, and machine learning.

Share this Article

๐Ÿ’ฌ Comments

Join the discussion