synchronized Keyword
The synchronized keyword is Java's built-in mechanism for declaring that a block of code or an entire method must execute under a monitor lock. It can be applied to instance methods (locking on 'this'), static methods (locking on the Class object), or explicit blocks (locking on any object reference). Every synchronized construct has exactly one associated lock object, and the JVM guarantees that at most one thread holds that lock at a time. When a thread enters a synchronized context, it acquires the lock, executes the body, releases the lock on exit (always, even via exception), and establishes a happens-before relationship that makes its writes visible to any thread that subsequently acquires the same lock. This entry covers all four syntactic forms, the scope and granularity implications of each, how synchronized interacts with inheritance and overriding, the common mistake of synchronizing on non-final fields and new objects, lock identity and why object references matter, performance implications of broad vs narrow synchronization, and the relationship between synchronized and volatile.
The Four Forms of synchronized
// ── Form 1: synchronized instance method — lock is 'this' ───────────
public class BankAccount {
private double balance;
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
public synchronized void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
balance += amount; // protected by 'this' monitor
}
public synchronized void withdraw(double amount) {
if (amount > balance) throw new IllegalStateException("Insufficient funds");
balance -= amount; // protected by 'this' monitor
}
public synchronized double getBalance() {
return balance; // synchronized read — visibility guaranteed
}
}
BankAccount account = new BankAccount(1000.0);
// Multiple threads can safely call deposit/withdraw — 'this' lock serializes them
// ── Form 2: synchronized static method — lock is the Class object ─────
public class IdGenerator {
private static long nextId = 0;
// Acquires IdGenerator.class monitor:
public static synchronized long next() {
return ++nextId;
}
// Instance method — independent lock, does NOT block static method:
public synchronized String format(long id) {
return String.format("ID-%06d", id);
}
}
long id1 = IdGenerator.next(); // safe from multiple threads — class lock
long id2 = IdGenerator.next();
System.out.println(id1 + " " + id2); // 1 2
// ── Form 3: synchronized block on this — narrowed scope ───────────────
public class LoggingCounter {
private int count = 0;
private final StringBuilder log = new StringBuilder();
public void increment(String message) {
// Expensive logging — no need to hold lock during this:
String entry = "[" + System.nanoTime() + "] " + message;
// Only the count update needs mutual exclusion:
synchronized (this) {
count++;
log.append(entry).append('
');
}
// Post-processing — lock already released:
System.out.println("Logged: " + message);
}
}
// ── Form 4: synchronized block on explicit lock object ────────────────
public class TwoGroupCounter {
private int countA = 0;
private int countB = 0;
// Two independent locks — threads in group A and group B don't block each other:
private final Object lockA = new Object();
private final Object lockB = new Object();
public void incrementA() {
synchronized (lockA) {
countA++; // lockA protects countA only
}
}
public void incrementB() {
synchronized (lockB) {
countB++; // lockB protects countB — independent of lockA
}
}
// Needs BOTH locks to read consistently:
public int total() {
synchronized (lockA) {
synchronized (lockB) {
return countA + countB; // consistent read of both
}
}
}
}
TwoGroupCounter tgc = new TwoGroupCounter();
// Thread 1 calling incrementA() and Thread 2 calling incrementB() proceed in parallel
// They only contend when total() is called (acquires both locks)Lock Identity, Common Mistakes, and synchronized with Inheritance
// ── Lock identity — the object reference matters ─────────────────────
public class WrongLock {
private Object lock = new Object(); // NON-FINAL — dangerous
public void methodA() {
synchronized (lock) { // Thread A reads lock → Object@1234
lock = new Object(); // lock reassigned mid-execution!
doWork();
}
}
public void methodB() {
synchronized (lock) { // Thread B might read Object@5678 (new object)
doWork(); // no mutual exclusion with methodA!
}
}
}
// CORRECT — final guarantees same object always:
public class CorrectLock {
private final Object lock = new Object(); // FINAL — always same object
public void methodA() {
synchronized (lock) { doWork(); } // always Object@1234
}
public void methodB() {
synchronized (lock) { doWork(); } // always Object@1234 — mutual exclusion ✓
}
}
// ── Synchronizing on 'new Object()' — zero protection ─────────────────
public void brokenSync() {
synchronized (new Object()) { // Each thread creates its own object
count++; // No mutual exclusion — every thread gets its own lock
}
}
// ── String literal interning hazard ──────────────────────────────────
public class LibraryA {
public void process() {
synchronized ("LOCK") { // uses the interned String "LOCK" from pool
doWork();
}
}
}
public class LibraryB {
public void process() {
synchronized ("LOCK") { // SAME interned String — shares monitor with LibraryA!
doOtherWork(); // LibraryA and LibraryB unexpectedly block each other
}
}
}
// CORRECT — use private final Object:
public class LibraryC {
private static final Object LOCK = new Object(); // unique object, not interned
public void process() {
synchronized (LOCK) { doWork(); } // no interference with other classes
}
}
// ── Integer autoboxing — cache range hazard ───────────────────────────
Integer a = 127; // cached — always same object
Integer b = 127;
System.out.println(a == b); // true — same cached object
Integer c = 128; // NOT cached — new object
Integer d = 128;
System.out.println(c == d); // false — different objects!
// synchronized on Integer 127: unintentionally shared across all code using 127
// synchronized on Integer 128: different threads get different objects — no exclusion
// ── synchronized not inherited by overriding ─────────────────────────
public class Parent {
public synchronized void criticalMethod() {
System.out.println("Parent: synchronized");
}
}
public class Child extends Parent {
@Override
public void criticalMethod() { // NOT synchronized — explicit override
System.out.println("Child: NOT synchronized");
super.criticalMethod(); // super call IS synchronized (on 'this')
}
}
// Child.criticalMethod() can be called by multiple threads concurrently
// The super.criticalMethod() call is protected, but Child's own code before it is not
// ── Correct pattern: always re-declare synchronized in override if needed ──
public class ChildCorrect extends Parent {
@Override
public synchronized void criticalMethod() { // explicit synchronized
System.out.println("ChildCorrect: synchronized");
super.criticalMethod(); // reentrant — same lock 'this'
}
}Scope, Granularity, Performance, and synchronized vs volatile
// ── Granularity: broad vs narrow scope ───────────────────────────────
public class OrderProcessor {
private final List<String> orders = new ArrayList<>();
private int processedCount = 0;
// BROAD: holds lock for entire method including I/O — poor throughput:
public synchronized void processOrderBroad(String orderId) {
String data = fetchFromDatabase(orderId); // I/O while holding lock!
orders.add(data);
processedCount++;
}
// NARROW: only the state updates are locked — I/O runs concurrently:
public void processOrderNarrow(String orderId) {
String data = fetchFromDatabase(orderId); // I/O without lock — parallel
synchronized (this) { // lock held only for state update
orders.add(data);
processedCount++;
} // lock released immediately — other threads can proceed
}
private String fetchFromDatabase(String id) {
try { Thread.sleep(10); } catch (InterruptedException e) {} // simulated I/O
return "Order:" + id;
}
}
// ── volatile for visibility only — flags and single-write fields ───────
public class TaskRunner {
private volatile boolean cancelled = false; // volatile: no sync overhead
public void run() {
while (!cancelled) { // always reads fresh value from main memory
doUnitOfWork();
}
System.out.println("Task cancelled");
}
public void cancel() {
cancelled = true; // immediately visible to run() — no sync needed
}
}
// ── volatile does NOT fix compound operations ─────────────────────────
public class BrokenVolatileCounter {
private volatile int count = 0;
public void increment() {
count++; // STILL NOT ATOMIC — read(count), add 1, write(count) are separate
// volatile only guarantees each individual read/write is visible
// It does NOT make the compound read-modify-write sequence atomic
}
}
// ── synchronized fixes both atomicity AND visibility ──────────────────
public class CorrectCounter {
private int count = 0; // does NOT need to be volatile — sync handles visibility
public synchronized void increment() { count++; } // atomic + visible
public synchronized int get() { return count; } // reads consistent value
}
// ── AtomicInteger: atomic compound operations without synchronized ─────
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); } // lock-free, atomic
public int get() { return count.get(); }
public int getAndReset(){ return count.getAndSet(0); } // atomic swap
}
// ── Double-checked locking — requires volatile ────────────────────────
public class Singleton {
// volatile needed: without it, the reference write might be seen
// before the constructor completes (due to instruction reordering):
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // first check — no lock (fast path)
synchronized (Singleton.class) {
if (instance == null) { // second check — under lock
instance = new Singleton(); // volatile write — constructor completes before reference is visible
}
}
}
return instance; // volatile read — always sees fully constructed object
}
}
// ── Performance comparison: synchronized vs volatile vs atomic ────────
// Uncontested synchronized: ~20-40ns per operation (memory barrier cost)
// Volatile read/write: ~5-15ns per operation (memory barrier, no lock)
// AtomicInteger CAS: ~10-25ns per operation (hardware CAS, no OS involvement)
// Contended synchronized: ~200-1000ns+ (OS thread scheduling dominates)
// Rule: prefer volatile or Atomic for single-variable operations;
// use synchronized for compound multi-variable state.