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
thisnever protectsstaticfields, 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 synchronized | Static synchronized | |
|---|---|---|
| Lock object | The specific instance (this) | The Class object (Foo.class) |
| Scope | Per-object | Across all instances |
| Protects | Instance fields | static fields |
| Two different instances | Do not block each other | Do block each other |
| Syntax | public 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 aTaskGeneratorobject, 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.classin 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, orLongAdderfor simple counters; they use lock-free CAS operations and are faster under contention. - You need more flexibility — consider ReentrantLock with a static
ReentrantLockfield for timed or interruptible locking.
Related Topics
- 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
synchronizedwith 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.