Question 6 · Section 9

What is the difference between synchronized method and synchronized block?

Both options provide synchronization, but they differ in what exactly is locked and which object is used as the lock.

Language versions: English Russian Ukrainian

Junior Level

Basic Understanding

Both options provide synchronization, but they differ in what exactly is locked and which object is used as the lock.

synchronized method

public class Counter {
    private int count = 0;

    // Locks the ENTIRE method
    // Lock object: this (current instance)
    public synchronized void increment() {
        count++;
    }

    // Static method
    // Lock object: Counter.class (class object)
    public static synchronized void resetAll() {
        // ...
    }
}

synchronized block

public class Counter {
    private int count = 0;
    private final Object lock = new Object();

    public void increment() {
        // Locks ONLY this block
        // Lock object: any specified object
        synchronized(lock) {
            count++;
        }
    }
}

Comparison Table

Characteristic synchronized method synchronized block
Lock object Always this (or Class) Any object
Lock scope Entire method Only the specified block
Readability Higher (declarative) Lower (more nesting)
Flexibility Low High
Safety Lower (any external code can acquire this and block the method) Higher (private lock object is inaccessible from outside, less deadlock risk)

When to use which

  • Method — when the entire method must be atomic and there’s no issue with external access to this
  • Block — when only part of the method needs synchronization, or you need control over the lock object

Middle Level

Bytecode-Level Differences

synchronized block

Uses explicit monitorenter and monitorexit instructions:

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

Bytecode:

public void increment();
  Code:
     0: aload_0
     1: getfield      #2  // Field lock:Ljava/lang/Object;
     4: dup
     5: astore_1
     6: monitorenter        // ← ACQUIRE
     7: aload_0
     8: dup
     9: getfield      #3    // Field count:I
    12: iconst_1
    13: iadd
    14: putfield      #3    // Field count:I
    17: aload_1
    18: monitorexit         // ← RELEASE (normal exit)
    19: goto          27
    22: astore_2
    23: aload_1
    24: monitorexit         // ← RELEASE (on exception)
    25: aload_2
    26: athrow
    27: return

synchronized method

Uses the ACC_SYNCHRONIZED flag in method attributes:

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

Bytecode (note — NO monitorenter/monitorexit!):

public synchronized void increment();
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED  // ← Synchronization flag
  Code:
    stack=3, locals=1, args_size=1
       0: aload_0
       1: dup
       2: getfield      #2  // Field count:I
       5: iconst_1
       6: iadd
       7: putfield      #2  // Field count:I
      10: return

The JVM automatically acquires and releases the monitor when calling a method with this flag.

ACC_SYNCHRONIZED — a flag in bytecode (ACC = access flags). The JVM sees this flag and automatically wraps the method call in monitor enter/exit.

Granularity (Scope)

// BAD: synchronized method locks the entire method
public synchronized void processOrder(Order order) {
    validate(order);        // Doesn't require locking
    calculatePrice(order);  // Doesn't require locking
    updateInventory(order); // Requires locking
    sendNotification(order);// Doesn't require locking (I/O operation!)
}

// GOOD: synchronized block only for the critical section
public void processOrder(Order order) {
    validate(order);         // Without lock
    calculatePrice(order);   // Without lock

    synchronized(inventoryLock) {
        updateInventory(order); // Only this is locked
    }

    sendNotification(order); // Without lock
}

Private Locks

public class SafeCounter {
    // BAD: someone outside could do synchronized(counter)
    public synchronized void increment() {
        count++;
    }

    // GOOD: private lock — no one outside can acquire it
    private final Object lock = new Object();

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

Why private lock is better:

  1. Encapsulation — external code cannot block your object
  2. Deadlock prevention — external code cannot create a deadlock with your lock
  3. Starvation prevention — external code cannot starve your methods
  4. Multiple locks — you can have different locks for different resources

Multiple Locks in One Class

public class UserProfileCache {
    private final Map<Integer, Profile> profiles = new HashMap<>();
    private final Map<Integer, Avatar> avatars = new HashMap<>();

    // Separate locks for independent resources
    private final Object profilesLock = new Object();
    private final Object avatarsLock = new Object();

    public Profile getProfile(int id) {
        synchronized(profilesLock) {
            return profiles.get(id);
        }
    }

    public Avatar getAvatar(int id) {
        synchronized(avatarsLock) {
            return avatars.get(id);
        }
    }

    // Both locks simultaneously
    public void clearAll() {
        synchronized(profilesLock) {
            synchronized(avatarsLock) {
                profiles.clear();
                avatars.clear();
            }
        }
    }
}

When synchronized method is a reasonable choice

  1. Small methods — the entire method needs to be atomic
  2. Inner classes — no external access to this
  3. No contention — only one thread accesses the object

When synchronized block is better: when you need to lock only part of the method, or when you need a separate lock object.


Senior Level

Under the Hood: ACC_SYNCHRONIZED Flag

A synchronized method does NOT contain monitorenter/monitorexit instructions in bytecode. Instead, the JVM checks the flag on method call:

// JVM pseudocode on method call:
if (method.hasFlag(ACC_SYNCHRONIZED)) {
    monitor.enter(method.isStatic() ? clazz : receiver);
    try {
        invokeMethod();
    } finally {
        monitor.exit(method.isStatic() ? clazz : receiver);
    }
}

This works slightly faster (difference ~0-5%, not noticeable in practice), as it doesn’t require additional instructions in the method body.

JIT Compiler Optimizations

Lock Coarsening

// Before optimization:
for (int i = 0; i < items.size(); i++) {
    synchronized(lock) {
        total += items.get(i).price;
    }
}

// After Lock Coarsening:
synchronized(lock) {
    for (int i = 0; i < items.size(); i++) {
        total += items.get(i).price;
    }
}

JIT merges many locks on the same object into one big lock.

Important: This only works if the JIT can prove that inside the loop there are no points where another thread could see an intermediate state.

Lock Elision

public void process() {
    // This object does NOT escape the method
    StringBuilder sb = new StringBuilder();
    synchronized(sb) { // JIT will remove this lock!
        sb.append("data");
    }
}

Escape Analysis determines:

  1. GlobalEscape — object is returned or stored in a field → lock NOT removed
  2. ArgEscape — object passed as argument but not stored → lock may be removed
  3. NoEscape — object is fully local → lock removed

For synchronized(this), this works less often, since this almost always “escapes”.

Comparison Table (Advanced)

Characteristic synchronized method synchronized block
Bytecode ACC_SYNCHRONIZED flag monitorenter/exit instructions
Lock object Always this or Class Any object (prefer private final)
Reliability Lower (public lock) Higher (encapsulated lock)
Lock Elision Works rarely Works often
Granularity Entire method Only critical section
I/O inside Blocks during I/O Can move I/O outside the lock

Performance and Highload

Avoid Static Synchronized

// VERY BAD: class-level lock
public class Config {
    private static Map<String, String> settings = new HashMap<>();

    public static synchronized void set(String key, String value) {
        settings.put(key, value); // Blocks EVERYONE using Config.class
    }
}

This is the most dangerous kind of synchronization — it can paralyze the entire system.

Solution:

public class Config {
    private static final Map<String, String> settings = new ConcurrentHashMap<>();
    private static final Object settingsLock = new Object();

    public static void set(String key, String value) {
        settings.put(key, value); // ConcurrentHashMap — lock-free
    }
}

Performance Impact

Operation Time Note
Method (uncontended) ~10-20 ns ACC_SYNCHRONIZED overhead
Block (uncontended) ~10-20 ns monitorenter/exit
Method (contended) ~1000+ ns Context switch
Block (contended) ~1000+ ns Context switch
Block (private lock) ~10-20 ns Less contention

Diagnostics

javap -v

javap -v MyClass.class

For a method:

public synchronized void method();
  descriptor: ()V
  flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED  // ← Flag here

For a block:

public void method();
  Code:
    ...
    monitorenter   // ← Instruction here
    ...
    monitorexit    // ← And here

Thread Dumps

"thread-1" BLOCKED on object monitor
  - waiting to lock <0x000000076af0c8d0> (a com.example.MyClass)
  // For method: shows it's this (MyClass)
  // For block: shows the specific lock object

Best Practices

  1. Prefer synchronized block — more control
  2. Use private final Object lock — encapsulation
  3. Minimize lock scope — don’t block I/O
  4. Avoid synchronized(this) — someone outside could block
  5. Avoid static synchronized — class-level locking is dangerous
  6. Consider ConcurrentHashMap — often better than synchronized HashMap
  7. Monitor contention — if growing, switch to segmented locks

Interview Cheat Sheet

Must know:

  • synchronized method uses ACC_SYNCHRONIZED flag, block uses monitorenter/monitorexit instructions
  • synchronized method locks the entire method on this (or Class for static); block locks only the specified part on any object
  • Private lock (private final Object lock) is safer: external code cannot acquire your lock and create a deadlock
  • JIT applies Lock Elision more often to synchronized blocks (local object) than to synchronized(this)
  • Granularity: block allows moving I/O and long operations outside synchronization
  • Uncontended performance is the same (~10-20 ns), the difference is in flexibility and safety

Frequent follow-up questions:

  • Why is synchronized(this) dangerous? — Any external code can do synchronized(obj) and block your methods
  • Can you have multiple locks in one class? — Yes, different private final Object for different resources (profiles, avatars, etc.)
  • When is synchronized method a reasonable choice? — Small methods, entire method is atomic, no external access to this
  • Why is static synchronized dangerous? — Class-level lock (ClassName.class) — one lock for ALL instances

Red flags (do NOT say):

  • “Synchronized method is faster than block” — no, performance is the same when uncontended
  • “Synchronized(this) is safe if the class is internal” — generally no, encapsulation is broken
  • “Monitorenter exists in synchronized method” — no, there’s the ACC_SYNCHRONIZED flag
  • “Private lock is only for aesthetics” — no, it prevents external deadlock and starvation

Related topics:

  • [[4. What is monitor in Java]] — intrinsic monitor mechanism
  • [[5. How does synchronized work at monitor level]] — lock state evolution
  • [[7. What is reentrant lock]] — ReentrantLock as a more flexible alternative
  • [[1. What is the difference between synchronized and volatile]] — when synchronization is needed at all