🧵 Concurrency Fundamentals · Advanced

synchronized in Java

Intrinsic locks on methods and blocks, mutual exclusion, reentrancy.

🧩 The mysteryA cafe with one bathroom key: whoever holds the key goes in, everyone else waits. Every Java object carries a key just like it. Here's how to use it.

Every object has a lock

Every Java object has an intrinsic lock (a monitor). synchronized (obj) { ... } acquires it, runs the block, and releases it, even if an exception is thrown. Only one thread can hold it at a time: mutual exclusion. And the next holder sees all the previous holder's writes.

private final Object lock = new Object();
private int count;
 
void inc() {
    synchronized (lock) {
        count++;
    }
}
🔮 Predict it

Your turn

Two threads each increment n[0] 750 times, always inside synchronized (lock). What does main print?

int[] n = {0};
Object lock = new Object();
Runnable r = () -> {
    for (int i = 0; i < 750; i++)
        synchronized (lock) { n[0]++; }
};
Thread a = new Thread(r), b = new Thread(r);
a.start(); b.start();
a.join(); b.join();
System.out.println(n[0]);
  1. 1500
  2. 750
  3. A different number on each run
Show the answer

Always 1500. Every n[0]++ happens while holding the same lock, so the read-modify-write can't interleave and nothing is lost. After both join()s, main is guaranteed to see the final value.

Which lock does a method take?

A synchronized instance method locks on this. A static synchronized method locks on the Class object, Account.class. Those are two different locks, so the two methods do not exclude each other: a trap when both touch the same static field.

class Account {
    // lock: this
    synchronized void deposit() { }
 
    // lock: Account.class
    static synchronized void audit() { }
}
⚠️ The trap

Same lock, every access

Locking only the writes isn't enough. A reader without the lock has no visibility guarantee and may even see a HashMap mid-resize. Every access, reads included, must use the same lock. Also note: threads locking *different* objects (listA, listB) don't exclude each other at all.

synchronized void add(String k) {
    stock.merge(k, 1, Integer::sum);
}
int count(String k) {   // no lock: bug!
    return stock.getOrDefault(k, 0);
}
🔮 Predict it

Can a thread block itself?

down() is synchronized and calls itself while already holding the lock. What happens?

class Counter {
    synchronized int down(int n) {
        return n == 0 ? 0 : 1 + down(n - 1);
    }
}
void main() {
    System.out.println(new Counter().down(3));
}
  1. 3
  2. Hangs forever (deadlock)
  3. Throws IllegalMonitorStateException
Show the answer

It prints 3. Intrinsic locks are reentrant: a thread that already owns a lock can acquire it again without blocking itself. The JVM keeps a hold count and releases the lock when the count drops back to zero.

💼 In the real world

Keep critical sections small

Old classes like Vector, Hashtable and StringBuffer synchronize every method: safe, but slow under contention. That's why ArrayList, HashMap and StringBuilder are today's defaults. And never make a network call while holding a lock: every other thread piles up BLOCKED behind it.

Key takeaways

  1. A synchronized instance method locks on this
  2. A static synchronized method locks on the Class object
  3. Every access to shared state must use the SAME lock
  4. Reentrant: the owner can re-acquire without blocking itself
🤯 Did you know?

An uncontended lock is cheap: the JVM only builds a heavyweight monitor ("inflation") once threads actually compete for an object's lock.

Practice questions

What does this print?

int[] n = {0};
Object lock = new Object();
Runnable r = () -> {
    for (int i = 0; i < 1000; i++)
        synchronized (lock) { n[0]++; }
};
Thread a = new Thread(r), b = new Thread(r);
a.start(); b.start();
a.join(); b.join();
System.out.println(n[0]);
  1. 2000
  2. 1000
  3. A different number on each run
  4. Compile error
Check your answer

2000. Every increment runs while holding the same lock, so no update is lost. After both joins, main is guaranteed to see the final value.

What does this print?

class Account {
    synchronized void a() { b(); }
    synchronized void b() {
        System.out.println("b ran");
    }
}
void main() {
    new Account().a();
    System.out.println("done");
}
  1. b ran done
  2. It hangs forever (deadlock)
  3. Throws IllegalMonitorStateException
  4. done
Check your answer

b ran done. The thread already holds the Account's lock when a() calls b(). Intrinsic locks are reentrant, so it simply re-enters and the hold count goes up.

Next: one thread sets running = false, yet another loops forever as if it never saw the write. Welcome to visibility.