Java's synchronized blocks and monitor locks
Java gives every object a single built-in monitor lock—an intrinsic mutex that the JVM manages automatically. When you write synchronized(obj), the thread must acquire that monitor before entering the block, and releases it when the block exits (even if an exception gets thrown).
This is not a generic “make everything thread-safe” annotation. It has two precise, well-defined guarantees:
- Mutual exclusion—only one thread can hold the monitor for a given object at a time. Other threads that try to enter any synchronized block on that same object block until it is released.
- Atomicity of the critical section—the entire body between the braces executes as an indivisible step relative to every other thread holding that same monitor. No other thread can observe an intermediate state.
What it does not do: make individual reads or writes atomic, prevent deadlocks from inconsistent lock ordering, or replace careful reasoning about which object is used as the lock—you need a stable reference and consistent use across all threads.
The code
To see these guarantees in action, consider two counters sharing an int value. Ten threads each increment it 100 000 times. Without a monitor lock, ten threads read-modify-write the same plain variable simultaneously—a thread A can read a value just as thread B is about to write back its own incremented copy, and both store the same result. One of the two increments disappears silently.
Here are both variants in one file:
static class UnsafeCounter implements RunnableCounter {
int value = 0;
@Override
public void increment() {
// No lock — every thread reads, modifies, writes without protection
int current = this.value;
this.value = current + 1;
}
@Override
public int getValue() { return this.value; }
}
static class SafeCounter implements RunnableCounter {
int value = 0;
@Override
public void increment() {
// The synchronized block acquires the object’s intrinsic monitor lock.
// Only one thread can hold this lock at a time; others block until released.
synchronized (this) {
// Critical section: read-modify-write is now atomic
// w.r.t. other synchronized blocks on “this”
int current = this.value;
this.value = current + 1;
}
}
@Override
public int getValue() { return this.value; }
}
The full file also includes a harness that launches 10 threads simultaneously (via CountDownLatch) and runs each counter through it, printing the result side by side.
Running it
Running with ten concurrent threads and 100 000 increments per thread produces dramatically different results:
- Unsafe counter—far below the expected 1 000 000. The exact number varies from run to run because the race depends on thread scheduling, but tens of thousands of updates are lost every time. Multiple threads read the same stale
value, each computescurrent + 1, and the later write overwrites the earlier one. - Safe counter—exactly 1 000 000. Zero lost updates. The monitor on
thisserializes entry: only one thread can execute the block per instance at a time, so every read-modify-write completes before the next thread enters.
The synchronized version is slower (roughly 2–3× in this micro-benchmark) because threads spend time blocked waiting for the monitor instead of executing—but that is the cost of correctness.
Takeaway
A synchronized block gives you a single object-level mutex: it guarantees that no two threads can be inside any synchronized block on that same object at the same time, and that the entire block body executes atomically with respect to those threads. It does not make individual fields safe—it makes the sequence of operations inside the braces indivisible.