Definition
- Synchronized method –
public synchronized void debit()acquires the monitor lock onthis(the instance) when the method is entered and releases it when the method exits. For static methods, the lock is on theClassobject. The entire method body is the critical section. - Synchronized block –
synchronized(lockObject) { ... }acquires the lock on any specified object for only the enclosed code. Non-critical code before and after the block runs without holding the lock. - Granularity – the method approach is coarse (the whole method is locked, including code that does not touch shared state). The block approach is fine (only the code that modifies shared state is locked).
- Lock object choice – a dedicated
private final Object lock = new Object()is the safest choice because no external code can accidentally synchronize on it.
A synchronized method is like locking the entire bank branch every time one teller needs to access the vault. Nobody can do anything: not even use the photocopier or answer phones. A synchronized block is like locking only the vault door for only the time you are inside. Other tellers can use the photocopier, answer phones, and serve customers at other counters. Everything outside the vault continues normally.
Understanding the Concepts
What is a synchronized method?
A synchronized instance method acquires the monitor lock on this when the method begins and releases it when the method returns (normally or by exception).
package com.bank.service;
import java.math.BigDecimal;
public class AccountServiceV1 {
private BigDecimal balance = new BigDecimal("100000.00");
public synchronized void debit(BigDecimal amount) {
// Lock on 'this' acquired here
validateAmount(amount); // locked: no shared state accessed
logTransaction("DEBIT", amount); // locked: no shared state accessed
balance = balance.subtract(amount); // locked: THIS is the only line that needs it
notifyAudit("DEBIT", amount); // locked: no shared state accessed
// Lock released here
}
private void validateAmount(BigDecimal amount) { /* validation logic */ }
private void logTransaction(String type, BigDecimal amount) { /* logging */ }
private void notifyAudit(String type, BigDecimal amount) { /* audit call */ }
}
The problem: validateAmount, logTransaction, and notifyAudit do not access shared state, yet they run while holding the lock. Any other thread calling any synchronized method on the same object is BLOCKED for the entire duration.
What is a synchronized block?
A synchronized block locks a specific object for only the enclosed critical section. Code before and after the block runs without holding the lock.
package com.bank.service;
import java.math.BigDecimal;
public class AccountServiceV2 {
private final Object balanceLock = new Object();
private BigDecimal balance = new BigDecimal("100000.00");
public void debit(BigDecimal amount) {
validateAmount(amount); // NOT locked: runs freely
logTransaction("DEBIT", amount); // NOT locked: runs freely
synchronized (balanceLock) { // lock ONLY for balance modification
if (balance.compareTo(amount) < 0) {
throw new IllegalStateException("Insufficient funds");
}
balance = balance.subtract(amount);
}
notifyAudit("DEBIT", amount); // NOT locked: runs freely
}
private void validateAmount(BigDecimal amount) { /* validation logic */ }
private void logTransaction(String type, BigDecimal amount) { /* logging */ }
private void notifyAudit(String type, BigDecimal amount) { /* audit call */ }
}
Only the balance check and subtraction are locked. Validation, logging, and audit notification run in parallel across threads.
How do the two compare?
| Feature | Synchronized method | Synchronized block |
|---|---|---|
| Lock object | this (or Class for static) | Any specified object |
| Scope | Entire method body | Only the enclosed code |
| Granularity | Coarse | Fine |
| Multiple locks | No (always this) | Yes (different locks for different data) |
| Lock hold time | Longer (includes non-critical code) | Shorter (only critical section) |
Why is the synchronized block preferred?
Three reasons make the block superior in production code.
First, shorter lock hold time. The block locks only the read-modify-write operation on shared state, not the surrounding validation, logging, or notification. Second, specific lock objects. You can use different locks for different pieces of shared state, allowing unrelated operations to proceed in parallel. Third, private lock objects prevent external code from accidentally synchronizing on this.
package com.bank.service;
import java.math.BigDecimal;
public class AccountServiceV3 {
private final Object balanceLock = new Object();
private final Object auditLock = new Object();
private BigDecimal balance = new BigDecimal("100000.00");
private int auditCount = 0;
public void debit(BigDecimal amount) {
synchronized (balanceLock) {
balance = balance.subtract(amount);
}
// auditLock is independent: debit and audit do not block each other
synchronized (auditLock) {
auditCount++;
}
}
}
What is a static synchronized method?
A static synchronized method locks on the Class object (ClassName.class), not on any instance. This means all instances share the same lock.
package com.bank.service;
public class GlobalAudit {
// Locks on GlobalAudit.class
public static synchronized void runAudit() {
System.out.println("Running global audit");
}
// Equivalent block form
public static void runAuditExplicit() {
synchronized (GlobalAudit.class) {
System.out.println("Running global audit");
}
}
}
What are common mistakes?
The most dangerous mistake is synchronizing on an object that can change or that is shared unintentionally.
package com.bank.service;
public class LockMistakes {
// WRONG: not final, can be reassigned
private Object lock = new Object();
public void transfer() {
synchronized (lock) {
// Another thread reassigns: lock = new Object();
// Now threads synchronize on DIFFERENT objects: no mutual exclusion!
}
}
// WRONG: synchronized(this) exposes the lock to external code
public synchronized void debit() {
// External code can do: synchronized(accountService) { Thread.sleep(10000); }
// This blocks ALL synchronized methods on your object for 10 seconds!
}
// CORRECT: private final lock
private final Object safeLock = new Object();
public void safeDebit() {
synchronized (safeLock) {
// Private: no external code can access this lock
// Final: cannot be reassigned
}
}
}
What should a senior engineer know?
- Lock hold time is the key metric. A synchronized method that includes a 500ms database call holds the lock for 500ms. A synchronized block around only the 1ms balance update holds the lock for 1ms. The difference is 500x throughput.
- Bytecode:
synchronizedmethods use theACC_SYNCHRONIZEDflag in the method's access flags. Synchronized blocks use explicitmonitorenterandmonitorexitinstructions. - Lock coarsening: the JIT compiler may merge adjacent synchronized blocks on the same lock into a single block to reduce monitor entry/exit overhead.
- Lock elision: if escape analysis proves that an object never escapes the current thread, the JIT can eliminate the synchronization entirely.
Visual Overview
Synchronized Method (coarse): ┌──────────────────────────────────┐ │ synchronized void debit() │ │ LOCK acquired on 'this' │ │ ┌────────────────────────────┐ │ │ │ validateAmount() │ │ locked (unnecessary) │ │ logTransaction() │ │ locked (unnecessary) │ │ balance -= amount │ │ locked (needed) │ │ notifyAudit() │ │ locked (unnecessary) │ └────────────────────────────┘ │ │ LOCK released │ └──────────────────────────────────┘ Synchronized Block (fine): ┌──────────────────────────────────┐ │ void debit() │ │ validateAmount() (no lock) │ │ logTransaction() (no lock) │ │ ┌────────────────────────────┐ │ │ │ synchronized (balanceLock) │ │ │ │ balance -= amount │ │ locked (needed) │ └────────────────────────────┘ │ │ notifyAudit() (no lock) │ └──────────────────────────────────┘
Key Takeaway
A synchronized method locks on this (or the Class object for static methods) for the entire method body. A synchronized block locks a specific object for only the critical section. The block is preferred because it minimizes lock hold time, allows separate locks for separate data, and keeps the lock object private.
The key metric is lock hold time. If a synchronized method includes validation, logging, and a network call alongside the balance update, the lock is held for the entire duration. A synchronized block around only the balance update holds the lock for a fraction of that time. In a banking system handling 5,000 operations per second, the difference between a 500ms and a 1ms lock hold time is the difference between 2 operations per second and 5,000.
Remember one sentence: use synchronized blocks with a private final lock object to protect only the code that touches shared state, not the entire method.
---