Skip to content
Java synchronization 6 min read

Static Synchronization

When you synchronize a regular (instance) method, the lock is tied to a specific object — two threads using different instances of the same class never block each other. Sometimes that is not enough. If multiple threads share static (class-level) data, you need a lock that spans every instance: static synchronization.

The Problem: Instance Locks Don’t Protect Static Data

Consider a simple task queue where an ID counter is static — shared by every instance:

public class TaskGenerator {
    private static int nextId = 0;

    // NOT thread-safe: each instance has its own lock
    public synchronized int generateId() {
        return ++nextId; // reads and writes shared static field
    }
}

When two threads each hold a different TaskGenerator instance and call generateId() simultaneously, they each acquire their own instance lock. Neither thread blocks the other, so nextId is read and written without coordination — a classic race condition.

Warning: Synchronizing on this never protects static fields, because every instance has a separate lock. Static data needs a class-level lock.

The Fix: Lock on the Class Object

Every Java class has exactly one java.lang.Class object loaded by the JVM. That object acts as a class-wide monitor — perfect for protecting static state. You can reach it in two ways.

Synchronized Static Methods

Adding synchronized to a static method makes the JVM lock on TaskGenerator.class instead of this:

public class TaskGenerator {
    private static int nextId = 0;

    public static synchronized int generateId() {
        return ++nextId; // now protected by the class-level lock
    }
}

Any thread calling TaskGenerator.generateId() — regardless of which instance it holds — must acquire TaskGenerator.class’s lock first.

Synchronized Block on .class

You can also lock explicitly in a block, which is useful when you want to protect only a short critical section inside a longer static method:

public class TaskGenerator {
    private static int nextId = 0;

    public static int generateId() {
        // non-critical pre-work could go here ...
        synchronized (TaskGenerator.class) {
            return ++nextId;
        }
    }
}

Both approaches are equivalent in what they lock; the block form gives you finer-grained control over how long you hold the lock.

Complete Working Example

public class PrintTask implements Runnable {
    private String name;

    public PrintTask(String name) {
        this.name = name;
    }

    // All threads, all instances, share one lock: PrintTask.class
    public static synchronized void printTable(String owner, int n) {
        for (int i = 1; i <= 5; i++) {
            System.out.println(owner + " : " + n + " x " + i + " = " + (n * i));
            try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        }
    }

    @Override
    public void run() {
        printTable(name, name.equals("Thread-A") ? 3 : 7);
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new PrintTask("Thread-A"));
        Thread t2 = new Thread(new PrintTask("Thread-B"));
        t1.start();
        t2.start();
    }
}

Output (Thread-A finishes entirely before Thread-B begins, never interleaved):

Thread-A : 3 x 1 = 3
Thread-A : 3 x 2 = 6
Thread-A : 3 x 3 = 9
Thread-A : 3 x 4 = 12
Thread-A : 3 x 5 = 15
Thread-B : 7 x 1 = 7
Thread-B : 7 x 2 = 14
...

Without static synchronized, the two threads would interleave their output unpredictably.

Instance Lock vs. Class Lock — Side-by-Side

It helps to see the distinction clearly:

Instance synchronizedStatic synchronized
Lock objectThe specific instance (this)The Class object (Foo.class)
ScopePer-objectAcross all instances
ProtectsInstance fieldsstatic fields
Two different instancesDo not block each otherDo block each other
Syntaxpublic synchronized void foo()public static synchronized void foo()

Note: Instance locks and class locks are completely independent. A thread holding TaskGenerator.class’s lock does not block a thread trying to acquire an instance lock on a TaskGenerator object, and vice versa.

Mixing Instance and Static Synchronized Methods

Because the two lock objects are different, you can have a situation where a static synchronized method and an instance synchronized method run concurrently without blocking each other — even on the same class. Keep this in mind when your class has both static and instance state to protect:

public class Shared {
    private static int classCount = 0;
    private int instanceCount = 0;

    // Locks on Shared.class
    public static synchronized void incrementClass() {
        classCount++;
    }

    // Locks on 'this'
    public synchronized void incrementInstance() {
        instanceCount++;
    }

    // These two methods CAN run in parallel — they use different locks!
}

If classCount and instanceCount are truly independent, this is perfectly fine. If either method reads or writes both counters, you have a design problem and should consolidate onto a single lock.

Under the Hood: The Class Object as a Monitor

When the JVM loads a class (see Class Loaders), it creates a java.lang.Class instance on the heap and stores a reference to it in the method area. Like any other Java object, this Class instance has an object header containing a monitor word.

synchronized static methods compile to the same monitorenter / monitorexit bytecode instructions as instance methods — the only difference is the target object. You can verify this with the javap -c -verbose tool:

// instance method
0: aload_0          // push 'this' onto stack
1: monitorenter

// static method
0: ldc  #2          // push Class literal (TaskGenerator.class)
2: monitorenter

The JVM’s lock escalation path is identical: biased locking → thin lock → inflated OS mutex, depending on contention level. Because Class objects are long-lived singletons, static locks are slightly more likely to be contended under high thread counts compared to short-lived instance locks — one more reason to keep the critical section as small as possible.

Using a Dedicated Static Lock Object

A common idiom in production code is to avoid using Foo.class directly and instead hold a private static lock object. This prevents external code from synchronizing on the same object (which could cause subtle deadlocks):

public class SafeRegistry {
    private static final Object LOCK = new Object();
    private static int count = 0;

    public static void register() {
        synchronized (LOCK) {
            count++;
        }
    }

    public static int getCount() {
        synchronized (LOCK) {
            return count;
        }
    }
}

Tip: Prefer private static final Object LOCK = new Object() over locking on .class in library code. It keeps the lock encapsulated and makes it impossible for callers to interfere.

When to Use Static Synchronization

Use static synchronization when:

  • Your class has static mutable state (counters, registries, caches, singletons) that multiple threads access simultaneously.
  • You need all instances to serialize around a shared resource (e.g., a static connection pool or a static file handle).
  • You are implementing the Singleton pattern in a multithreaded environment.

Avoid it when:

  • The data is per-instance — use instance synchronization instead (see Synchronization).
  • You need higher performance — consider AtomicInteger, AtomicLong, or LongAdder for simple counters; they use lock-free CAS operations and are faster under contention.
  • You need more flexibility — consider ReentrantLock with a static ReentrantLock field for timed or interruptible locking.
  • Synchronization — The full picture of Java’s synchronization model, monitors, and the happens-before relationship.
  • Synchronized Block — Fine-grained locking on any object, including SomeClass.class, instead of locking an entire method.
  • Deadlock — How holding multiple locks (including class-level ones) in the wrong order leads to threads waiting forever.
  • ReentrantLock & Monitors — A flexible alternative to synchronized with tryLock, timed lock, and condition variables.
  • volatile Keyword — Lightweight visibility guarantee for a single static field when you don’t need mutual exclusion.
  • Java Memory Model — The formal rules that make static synchronization’s memory visibility guarantees possible.
Last updated June 13, 2026
Was this helpful?