When several threads access the same data at the same time, something can go wrong. synchronized is Java's first and most direct tool to prevent that.
What breaks without protection
Imagine a visit counter. Two threads read the value 42, both add 1, both write back 43. One of the two increments is lost — the result is 43 instead of 44. This is a race condition.
class Counter {
private int value = 0;
void increment() {
value++; // read-modify-write — not atomic
}
int get() {
return value;
}
}
The value++ operation is actually three separate steps: read, add, write. Between any two of them another thread can step in.
A critical section is a piece of code that only one thread may execute at any given moment. synchronized is a way to mark this section.
What a monitor is
Every object in Java has a built-in monitor (also known as the intrinsic lock, mutex). A monitor is an invisible lock: the thread that has "acquired" the monitor enters the critical section; all other threads wait at the closed door.
A short formula: acquire the monitor → enter the section → release the monitor → the next one can enter.
A monitor always belongs to some object. Which object exactly depends on the form of synchronized.
synchronized method and synchronized block
Instance method: the monitor is this
class Counter {
private int value = 0;
synchronized void increment() {
value++; // only one thread at a time
}
synchronized int get() {
return value;
}
}
The keyword on a method means: acquire the monitor of the this object for the entire duration of the method. Two threads sharing one Counter instance cannot execute increment() or get() simultaneously.
Class method: the monitor is the Class object
class Registry {
private static int count = 0;
static synchronized void add() {
count++;
}
}
For a static method the monitor is the Registry.class object — one for the whole class, not per instance.
Block: the monitor is specified explicitly
class Cache {
private final Map<String, String> data = new HashMap<>();
private final Object lock = new Object(); // dedicated lock object
void put(String key, String value) {
synchronized (lock) {
data.put(key, value);
}
}
String get(String key) {
synchronized (lock) {
return data.get(key);
}
}
}
A block gives more control: you can synchronize only the fragment you need rather than the whole method, and explicitly choose the lock object.
Which object to synchronize on
The main rule here: all threads that access the same data must synchronize on the same object. If one thread acquires this and another acquires lock, they will not know about each other — the race remains.
Three options in practice:
| Option | When | Pros / cons |
|---|---|---|
synchronized (this) | small class, no external access to this | simple; but external code may accidentally acquire the same monitor |
synchronized (MyClass.class) | static fields | one lock per class; coarse granularity, easy to create a bottleneck |
synchronized (lock) where lock = new Object() | explicit control | recommended; nobody outside knows lock, accidental acquisition is impossible |
A dedicated lock object is the most predictable option: it is visible only inside the class and cannot be acquired from the outside.
Visibility through synchronized: happens-before
synchronized solves not only the order of access, but also the visibility of changes between threads.
In Java, changes made by one thread are not guaranteed to be visible to another immediately: the processor and the JVM are allowed to cache values and reorder instructions. The language specification defines this through the happens-before relationship: if operation A happens-before operation B, then B sees the result of A.
synchronized provides happens-before: releasing a monitor happens-before acquiring the same monitor by another thread.
class Shared {
private int x = 0;
private final Object lock = new Object();
void write() {
synchronized (lock) {
x = 42; // write
}
// released lock
}
int read() {
synchronized (lock) {
// acquired the same lock — we see x = 42
return x;
}
}
}
Without synchronized, reading x could return 0 — even if write() had already finished.
wait and notify — waiting for a condition
Sometimes a thread must wait until another thread does something. For this, wait() and notify() are used inside a synchronized block.
class Queue {
private final List<String> items = new ArrayList<>();
private final Object lock = new Object();
void produce(String item) {
synchronized (lock) {
items.add(item);
lock.notify(); // wake up one waiting thread
}
}
String consume() throws InterruptedException {
synchronized (lock) {
while (items.isEmpty()) {
lock.wait(); // release the monitor and wait
}
return items.remove(0);
}
}
}
wait() does three things at once: it releases the monitor, goes to sleep, and on wakeup re-acquires the monitor. That is exactly why both methods — wait() and notify() — can only be called inside a block synchronized on the same object.
The condition check (items.isEmpty()) is done in a while, not an if: a wakeup can be spurious (spurious wakeup), so after returning from wait() the condition must be checked again.
For complex producer–consumer scenarios it is more convenient today to use java.util.concurrent.BlockingQueue, but understanding wait/notify is the foundation of the mechanism.
Granularity and contention
Contention (competition for a lock) is a situation where a thread is forced to wait while another holds the monitor. The longer a thread holds the lock and the more threads want it, the stronger the contention — and the more time threads spend waiting.
A few rules that help reduce it:
- Synchronize only what you have to. If you only need to protect the update of one field — don't acquire the monitor for the entire method with expensive computations.
- Don't call foreign code under the lock. Calling an external method under
synchronizedrisks a deadlock or an unexpectedly long hold. - Keep the block short. Anything that can be computed before or after
synchronized, move outside it.
void process(String input) {
String result = compute(input); // long work — without the lock
synchronized (lock) {
data.add(result); // only the fast write under the lock
}
}
In short
- A critical section is a piece of code that only one thread executes at a time.
- Every Java object has a monitor (intrinsic lock);
synchronizedacquires it for the duration of the block or method. - Instance method → the
thismonitor; static method → theClassmonitor; block → the explicitly specified object. - All threads working with the same data must synchronize on one object.
synchronizedprovides happens-before: a thread that acquired the monitor sees everything done by the thread that released it earlier.wait()/notify()— waiting for a condition inside a synchronized block; do the condition check in awhile.- Keep the critical section short — a long monitor hold creates contention and slows down all waiting threads.
What to read next
- Race conditions and how to find them — in detail about how races manifest and methods to detect them.
- ReentrantLock and explicit locks — advanced features: tryLock, timeout, conditions, fairness.
- Bugs in concurrent code — deadlocks, livelocks and other typical problems.