JAVA / THREADS AND CONCURRENCY
synchronized methods and intrinsic locks
Explain what a synchronized method actually locks, predict which threads block each other, and use reentrancy without deadlocking your own object.
What you will learn
- Name the lock a synchronized method takes: the receiver, or the Class if static
- Predict which calls block: any two synchronized instance methods on one object
- Rely on reentrancy when a synchronized method calls another on the same object
- Spot instance locks that fail to guard static fields
Understanding synchronized methods and intrinsic locks
Writing synchronized in front of an instance method is equivalent to wrapping the entire body in a lock on the receiver: entering acquires that object's single intrinsic monitor, and every exit path releases it, including an early return or a thrown exception. The modifier therefore says almost nothing about the method itself; it names an object. Because each object has exactly one monitor, all synchronized instance methods of one object share one queue: while a thread runs account.deposit(...), a thread calling account.balance() waits, even though the two methods look unrelated.
A static synchronized method has no receiver, so it cannot lock this; it locks the Class object instead, Config.class for a class named Config. That is a different object from every instance, which means instance-level and static-level synchronized methods never exclude each other. This is why marking instance methods synchronized does nothing for state kept in a static field: two threads calling into two different instances take two different monitors and both walk into the same static variable.
Intrinsic locks are reentrant and counted per owning thread: an acquire by the thread that already owns the monitor just increments a hold count, and the monitor is handed on only when that count drops back to zero. Without that, any synchronized method calling another synchronized method on the same object, including super.save() from an overriding synchronized save(), would deadlock against itself. The other half of the guarantee is memory: releasing a monitor publishes what the thread wrote while holding it to whichever thread acquires that same monitor next, which is exactly what an unsynchronized getter forfeits, since it never acquires the monitor at all.
public class Main {
static class Counter {
private int count;
synchronized void increment() {
count++;
}
synchronized void incrementTwice() {
increment(); // reentrant: this thread already owns the monitor
increment();
}
synchronized int get() {
return count;
}
synchronized void showLock() {
System.out.println("inside a synchronized method, holdsLock = " + Thread.holdsLock(this));
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
counter.showLock();
System.out.println("back in main, holdsLock = " + Thread.holdsLock(counter));
Runnable job = () -> {
for (int i = 0; i < 5000; i++) {
counter.incrementTwice();
}
};
Thread one = new Thread(job);
Thread two = new Thread(job);
one.start();
two.start();
one.join();
two.join();
System.out.println("count = " + counter.get());
}
}A synchronized method is a lock on one particular object's single intrinsic monitor: the receiver for instance methods, the Class object for static ones.
Worked examples
Static synchronized locks the Class object
Shows that a static synchronized method holds Config.class while an instance method holds only the receiver.
public class Main {
static class Config {
static synchronized void reload() {
System.out.println("reload holds Config.class: " + Thread.holdsLock(Config.class));
}
synchronized void apply() {
System.out.println("apply holds this: " + Thread.holdsLock(this));
System.out.println("apply holds Config.class: " + Thread.holdsLock(Config.class));
}
}
public static void main(String[] args) {
Config.reload();
new Config().apply();
}
}Example explained
Line 1static synchronized void reload() acquires the monitor of Config.class, the one Class object for the type.
Line 2Inside apply() the calling thread owns the receiver's monitor, so holdsLock(this) reports true.
Line 3That same thread does not own Config.class, so a static and an instance synchronized method can run at the same time.
Line 4Consequence: synchronized instance methods give no protection at all to data stored in static fields.
The monitor is released on the exception path
Shows that throwing out of a synchronized method unlocks the object but does not undo partial changes.
public class Main {
static class Vault {
private int balance = 100;
synchronized void withdraw(int amount) {
if (amount > balance) {
throw new IllegalArgumentException("insufficient funds");
}
balance -= amount;
}
synchronized int balance() {
return balance;
}
}
public static void main(String[] args) {
Vault vault = new Vault();
try {
vault.withdraw(500);
} catch (IllegalArgumentException e) {
System.out.println("caught: " + e.getMessage());
}
System.out.println("main still holds the lock: " + Thread.holdsLock(vault));
vault.withdraw(30);
System.out.println("balance = " + vault.balance());
}
}Example explained
Line 1The throw leaves withdraw abnormally, and the JVM still runs the monitor release, so the lock cannot leak.
Line 2Thread.holdsLock(vault) is false afterwards, which is why the later withdraw(30) does not hang.
Line 3Unlocking is not a rollback: if the exception had been thrown after balance -= amount, the next thread would see the half-applied change.
One monitor per receiver, not per method
Two threads run the same synchronized method concurrently on different objects, but serialize on the same object.
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class Main {
static class Box {
synchronized void hold(CountDownLatch entered, CountDownLatch go) throws InterruptedException {
entered.countDown();
go.await();
}
}
static void enter(Box box, CountDownLatch entered, CountDownLatch go) {
try {
box.hold(entered, go);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
static boolean bothInsideAtOnce(Box first, Box second) throws InterruptedException {
CountDownLatch entered = new CountDownLatch(2);
CountDownLatch go = new CountDownLatch(1);
Thread t1 = new Thread(() -> enter(first, entered, go));
Thread t2 = new Thread(() -> enter(second, entered, go));
t1.start();
t2.start();
boolean both = entered.await(500, TimeUnit.MILLISECONDS);
go.countDown();
t1.join();
t2.join();
return both;
}
public static void main(String[] args) throws InterruptedException {
Box a = new Box();
Box b = new Box();
System.out.println("different receivers: " + bothInsideAtOnce(a, b));
System.out.println("same receiver: " + bothInsideAtOnce(a, a));
}
}Example explained
Line 1hold locks whichever object it was called on, so a and b provide two independent monitors and both threads sit inside the method together.
Line 2Passing a twice makes both threads contend for one monitor; the second is parked at the method entry, so the latch never reaches zero.
Line 3entered.await(500, TimeUnit.MILLISECONDS) returning false is the observable proof of mutual exclusion.
Line 4The second run still finishes because go.countDown() frees the first thread, and the second then enters and finds the latch already open.
Important notes
A constructor cannot be declared synchronized; the compiler rejects the modifier, since there is nothing to exclude until the reference escapes.
synchronized is not inherited, and it is not part of the signature: an override can leave it off with no warning, and any outside code holding your reference can do synchronized (yourObject) and stall all of its synchronized methods.
Common mistakes
Synchronizing the mutator but leaving the getter plain: the reading thread never acquires the monitor, so it can return a stale or in-between value, and the bug only appears under load.
Assuming different method names mean different locks and putting slow work in one synchronized method: every other synchronized method on that object serializes behind it.
Updating a static counter from synchronized instance methods on several instances: each instance locks itself, so the static field is still raced and the total comes out low.
Try it yourself
Change, predict, then run
Write a Ticker class whose synchronized instance method tick() increments both a private int count and a static int total, then run two threads doing 10000 ticks each on two separate Ticker objects. Print count for each object and total, explain why total falls short, and fix it by moving the total update into a static synchronized method.
Open the Java workspaceCheck your understanding
Counter declares synchronized void a(), synchronized void b(), and static synchronized void s(). Thread 1 is inside c.a() on instance c and stays there for a second. What can thread 2 do meanwhile?
- Call c.b() immediately, because only calls to the same method exclude each other
- Nothing on Counter at all, because holding one monitor of a class blocks the whole class
- Call Counter.s() immediately, but block on c.b()
- Call both c.b() and Counter.s() immediately, because a() locks only the field it writes
Show answer
a() and b() both acquire the monitor of the receiver c, so c.b() waits until a() returns; s() is static and acquires Counter.class, a different monitor, so it proceeds at once. Option 0 is the usual trap: the lock belongs to the object, not to the method, so distinct method names buy no independence.