The synchronized Keyword
The synchronized Keyword
In the previous lesson you saw how race conditions arise when multiple threads read and write shared state without coordination. Java's oldest and most fundamental answer to that problem is the synchronized keyword. Understanding it deeply — not just the syntax, but the memory model guarantees and the performance trade-offs — is essential before you reach for higher-level concurrency utilities.
The Monitor: Java's Built-In Lock
Every Java object carries a hidden field called a monitor (also called an intrinsic lock or object lock). You cannot see this field in source code; the JVM manages it. The monitor enforces two things simultaneously:
- Mutual exclusion — at most one thread can hold the monitor at any given time. Every other thread that tries to enter a synchronized section on that same monitor is blocked until the holder releases it.
- Memory visibility — when a thread releases a monitor, all writes it made are flushed to main memory. When a thread acquires the same monitor next, it sees those writes. This makes
synchronizeda happens-before boundary.
Class object). Two synchronized methods on the same instance share the same monitor and therefore cannot run concurrently. Two methods on different instances have separate monitors and can run concurrently.
Synchronized Methods
Add synchronized to a method declaration and the JVM automatically acquires this object's monitor on entry and releases it on exit — whether the method returns normally or throws an exception.
Without synchronized, count++ compiles to three bytecode instructions (read, add, write). A context switch between any two of them on a different thread causes a lost update — the classic race condition. Making both methods synchronized means only one thread can be inside either method at once.
Synchronized Blocks: Finer Granularity
A synchronized method locks this for its entire duration. A synchronized block lets you choose which object to lock and how much code to protect:
By locking on a private final Object rather than this, you make the lock invisible to callers. If you lock on this, external code can also synchronize on the same object and potentially cause unexpected contention or deadlock.
private final Object lock = new Object(); rather than locking on this or on the class. It prevents caller interference and makes the locking strategy explicit and readable.
Static Synchronized Methods
When a method is both static and synchronized, the monitor used is the Class object, not any instance. That lock is shared across all instances of the class.
Locking on the Class object is coarser than locking on an instance. Be careful not to mix static and instance synchronization if they protect the same state — they use different monitors and provide no mutual exclusion between them.
Reentrancy
Java's monitors are reentrant: a thread that already holds a lock can re-acquire it without blocking. This prevents a thread from deadlocking itself when a synchronized method calls another synchronized method on the same object.
When set calls audit, the same thread re-enters the monitor it already owns. The JVM maintains a per-thread entry count; the lock is released only when the count drops to zero.
Performance and Trade-offs
Every synchronized entry point is a potential bottleneck: all threads compete for the same lock and only one proceeds at a time. Consider these strategies to reduce contention:
- Keep critical sections short. Move I/O, network calls, or heavy computation outside the synchronized block.
- Use separate locks for independent state. If two fields are never accessed together, protect them with two distinct lock objects so threads can work on them in parallel.
- Consider read/write locking.
java.util.concurrent.locks.ReentrantReadWriteLockallows many concurrent readers when no writer is active — a win for read-heavy workloads. - Consider atomic variables. For single-variable counters,
AtomicLongorLongAdderoffer non-blocking throughput superior tosynchronized.
synchronized blocks are very cheap — often just a few nanoseconds. Contention is the real cost; reduce it by shrinking critical sections and splitting locks.
Summary
The synchronized keyword is Java's foundational mutual-exclusion primitive. Every object has a monitor; acquiring it guarantees both exclusion and memory visibility. Synchronized methods lock this; synchronized blocks let you choose the lock object and the exact scope. Prefer private lock objects, keep critical sections as narrow as possible, and remember that every synchronized read is just as necessary as every synchronized write. In the next lesson we will look at volatile — a lighter tool that provides visibility without exclusion.