Synchronization
The synchronized keyword uses an object's intrinsic monitor lock to enforce mutual exclusion, preventing the race conditions that occur when multiple threads read and write shared state without coordination.
When two or more threads read and write the same mutable state without coordination, the outcome can depend on the precise, unpredictable interleaving of their instructions — a race condition. Java's synchronized keyword is the original, built-in tool for preventing this: it uses every object's intrinsic monitor lock to guarantee that only one thread at a time can execute a protected section of code, and it also provides visibility guarantees so that changes made inside a synchronized block become visible to other threads that later synchronize on the same lock.
Race Conditions: Why Synchronization Is Needed
A race condition occurs when the correctness of a program depends on the relative timing of multiple threads accessing shared mutable state. The classic example is an unsynchronized counter: count++ looks like one operation but is actually three — read count, add 1, write count back — and if two threads interleave those three steps, an increment can be lost.
Without synchronization, the JVM gives no guarantee that interleaved read-modify-write sequences from different threads won't clobber each other's updates.
class Counter {
private int count = 0;
void increment() { count++; } // NOT atomic: read-modify-write
int get() { return count; }
}
// Two threads each calling increment() 100_000 times concurrently
// often produce a final count LESS than 200_000, due to lost updates.The Intrinsic Monitor Lock
Every Java object — not just objects specifically designed for locking — has an associated intrinsic lock, sometimes called a monitor lock. At any moment, at most one thread can hold a given object's intrinsic lock. The synchronized keyword is how code acquires and releases that lock automatically, around a block or method.
Only one thread may hold a given object's monitor at a time; other threads attempting to acquire the same monitor enter the BLOCKED state (covered in the Thread Lifecycle topic) until it is released.
Synchronized Methods
Marking an instance method synchronized makes the entire method body a critical section guarded by the intrinsic lock of the object the method is called on (the this reference). Only one thread at a time can be executing any synchronized instance method on the same object.
Both methods synchronize on the same object (this), so a call to increment() and a call to get() on the same Counter instance can never execute concurrently.
A static synchronized method locks on the Class object of the declaring class (e.g. Counter.class) instead of an instance, since there is no this for a static method.
All threads calling IdGenerator.next(), regardless of which instance (if any) they hold, contend for the same class-level lock.
class Counter {
private int count = 0;
synchronized void increment() { // locks on 'this'
count++;
}
synchronized int get() { // also locks on 'this'
return count;
}
}class IdGenerator {
private static int nextId = 0;
static synchronized int next() { return nextId++; } // locks on IdGenerator.class
}Synchronized Blocks
synchronized(lockObject) { ... } locks on an explicitly chosen object, letting you protect only the specific statements that touch shared state rather than an entire method — reducing how long the lock is held and which code paths contend for it. This is generally preferred over synchronizing whole methods when only part of a method needs protection, or when a private, dedicated lock object should be used rather than exposing this as the lock.
Using a private final Object as the lock (rather than 'this') prevents external code from accidentally synchronizing on the same lock and causing unrelated contention or deadlocks.
class Account {
private final Object lock = new Object();
private double balance;
void deposit(double amount) {
// ... possibly some unrelated, non-shared-state work here ...
synchronized (lock) {
balance += amount; // only the critical section is protected
}
}
}Mutual Exclusion Guarantee
Mutual exclusion means that for a given lock object, the JVM guarantees at most one thread executes code synchronized on that lock at any instant, across all threads and all synchronized methods/blocks that use that same lock. This is what makes compound operations like check-then-act or read-modify-write safe when every access path is synchronized on the same object.
Mutual exclusion only holds among code paths synchronized on the same lock object. Two synchronized blocks that lock on different objects provide no mutual exclusion relative to each other — this is a very common source of bugs (synchronizing on 'the wrong lock', or on different locks per field).
Reentrancy
Java's intrinsic locks are reentrant: a thread that already holds a lock can acquire it again (e.g. by calling another synchronized method on the same object from within a synchronized method) without deadlocking itself. The JVM tracks a hold count per thread per lock and only fully releases the lock once the count returns to zero.
Without reentrancy, this pattern would deadlock the calling thread against itself; because intrinsic locks are reentrant, it works correctly.
class Reentrant {
synchronized void outer() {
System.out.println("in outer");
inner(); // re-acquires the same lock on the same thread -- fine
}
synchronized void inner() {
System.out.println("in inner");
}
}Visibility Basics
Synchronization does more than serialize access — it also establishes a happens-before relationship. When a thread releases a lock, all writes it made before releasing become visible to any thread that subsequently acquires the same lock. Without this guarantee, a thread's CPU cache or the compiler's optimizations could let one thread never observe another thread's writes to shared fields, even absent a true data race on timing.
Both the write and the read go through the same lock, so a thread calling isReady() after another thread called setReady() (with the corresponding happens-before edge established) is guaranteed to see ready == true, not a stale cached value.
class Flag {
private boolean ready = false;
private final Object lock = new Object();
void setReady() {
synchronized (lock) { ready = true; }
}
boolean isReady() {
synchronized (lock) { return ready; }
}
}What Synchronized Does Not Do
synchronized provides mutual exclusion and visibility, but it does not guarantee fairness (which waiting thread gets the lock next is unspecified), does not prevent deadlock (two threads each holding a lock the other wants will block forever), and does not make an object 'thread-safe' automatically just because some of its methods are synchronized — every code path that touches the shared state must be synchronized on the same lock for the guarantees to hold.
Deadlock, fairness, and interruptible/timed lock acquisition are exactly what the Lock interface and ReentrantLock (a separate topic) were introduced to address, offering capabilities intrinsic locks do not.
Fixing the Race Condition
Returning to the earlier lost-update example, wrapping the read-modify-write in synchronized fixes it, because now the entire compound operation executes as one atomic-with-respect-to-other-threads unit.
Every access to count now goes through the same lock, giving both mutual exclusion (no lost updates) and visibility (get() always sees the latest write).
class Counter {
private int count = 0;
private final Object lock = new Object();
void increment() {
synchronized (lock) { count++; }
}
int get() {
synchronized (lock) { return count; }
}
}
// Now two threads each calling increment() 100_000 times reliably total 200_000.Practical Usage: Guarding Shared Mutable State
The general rule: any field that can be read by one thread and written by another must be protected — either via synchronization, an equivalent java.util.concurrent construct (locks, atomics, concurrent collections — later topics), or by not sharing mutable state across threads at all (e.g. confining it to one thread, or using immutable objects, which need no synchronization because they cannot change after construction).
Key Takeaways
A race condition arises when multiple threads access shared mutable state without coordination and the result depends on timing.
Every Java object has an intrinsic monitor lock; synchronized methods and blocks acquire and release it automatically.
Instance synchronized methods lock on this; static synchronized methods lock on the Class object.
synchronized guarantees mutual exclusion only among code paths that lock on the same object — locking on different objects provides no protection relative to each other.
Intrinsic locks are reentrant: a thread already holding a lock can acquire it again without deadlocking itself.
Synchronization also establishes a happens-before relationship, guaranteeing visibility of writes made before a lock release to threads that later acquire the same lock.
synchronized does not guarantee fairness and does not prevent deadlock.
synchronized (new Object()) { count++; // locks on a brand-new object every time -- no real exclusion } private final Object lock = new Object(); // one shared instance //... synchronized (lock) { count++; }
A new Object() is created fresh on every call, so each thread ends up locking on a different object, providing no mutual exclusion at all.
class Counter { private int a; private int b; synchronized void setA() { a++; } void setB() { synchronized (this) { b++; } } // ok, same lock void unsafeRead() { System.out.println(a); } // NOT synchronized -- visibility not guaranteed } class Counter { private int a; private int b; synchronized void setA() { a++; } synchronized void setB() { b++; } synchronized int readA() { return a; } // every access goes through the same lock }
Synchronizing only some accesses to a field does not guarantee the others see up-to-date values — every read and write of shared state needs to go through the same lock for the visibility and mutual-exclusion guarantees to apply consistently.
synchronized void withdraw() { synchronized (this) { lockAccountB(); } } // meanwhile another thread does the reverse order -- classic deadlock
// Establish and always follow a single, consistent lock-acquisition order // across all code paths that need multiple locks (e.g. always lock the // account with the lower ID first), or use tryLock with a timeout.
Acquiring multiple locks in inconsistent order across different threads is the classic cause of deadlock; synchronized has no built-in deadlock detection or avoidance.
What is a race condition, and why does count++ suffer from one when unsynchronized?
A race condition occurs when program correctness depends on the unpredictable relative timing of multiple threads accessing shared mutable state. count++ is actually three steps (read, add, write); if two threads interleave those steps, both can read the same old value and write back the same incremented value, silently losing one of the increments.
What object does a synchronized instance method lock on? What about a static synchronized method?
An instance synchronized method locks on 'this', the object the method was called on. A static synchronized method locks on the Class object of the declaring class (e.g. MyClass.class), because there is no instance receiver for a static method.
Why might a synchronized block be preferred over a synchronized method?
A synchronized block lets you protect only the specific statements that actually touch shared state, holding the lock for a shorter time and reducing contention, and lets you choose a dedicated private lock object instead of exposing 'this' (which external code could otherwise synchronize on, causing unintended contention).
What does it mean for Java's intrinsic locks to be reentrant, and why does that matter?
A thread that already holds a lock can acquire it again (e.g. calling another synchronized method on the same object from within a synchronized method) without blocking itself. The JVM tracks a per-thread hold count, releasing the lock fully only when it returns to zero. Without reentrancy, common patterns like one synchronized method calling another on the same object would deadlock the calling thread against itself.
Besides mutual exclusion, what other guarantee does synchronized provide, and why does that matter?
It also establishes a happens-before relationship: when a thread releases a lock, its writes made before release become visible to any thread that subsequently acquires the same lock. Without this, a thread could keep observing stale values of shared fields due to caching or reordering, even without any interleaving-timing race.
Does making some of a class's methods synchronized automatically make the class thread-safe? Explain.
No. Every code path that reads or writes the shared state must go through the same lock for the mutual-exclusion and visibility guarantees to hold consistently. If even one accessor bypasses synchronization, or two accessors lock on different objects, the class is not safely thread-safe.
Does synchronized prevent deadlock? What causes deadlock with intrinsic locks?
No, synchronized provides no deadlock prevention or detection. Deadlock typically arises when two or more threads each hold a lock the other needs and each is waiting to acquire the other's lock, usually from acquiring multiple locks in inconsistent order across different code paths.
Write a thread-safe BankAccount class with synchronized deposit and withdraw methods, where withdraw refuses to overdraw the balance.
public class BankAccount { private double balance; public BankAccount(double initialBalance) { this.balance = initialBalance; } public synchronized void deposit(double amount) { balance += amount; } public synchronized boolean withdraw(double amount) { if (amount > balance) { return false; // insufficient funds } balance -= amount; return true; } public synchronized double getBalance() { return balance; } }
Because deposit, withdraw, and getBalance all synchronize on the same lock (the implicit 'this' of the BankAccount instance), the check-then-act in withdraw (checking amount > balance, then mutating balance) executes atomically with respect to other threads calling any of these methods on the same instance.
Why is getUnsafe() a latent bug even though inc() is synchronized?
getUnsafe() bypasses the lock entirely, so it has no happens-before relationship with inc()'s writes — a thread calling getUnsafe() is not guaranteed to see the latest value of x written by another thread's inc() call, and could observe a stale cached value indefinitely under the Java Memory Model. It also risks reading x mid-update on some platforms if x were a 64-bit type not otherwise guaranteed atomic, though int reads/writes are themselves atomic — the real issue here is visibility, not tearing.
Trace through two threads calling increment() concurrently 1000 times each on an unsynchronized int counter, then show the synchronized fix and explain why it guarantees the correct total.
Unsynchronized: count++ is read-modify-write, not atomic. If Thread A reads count=57, then Thread B also reads count=57 before A writes back 58, both eventually write 58, silently losing one increment — over 2000 total calls, the final count is often less than 2000, non-deterministically. Fix: wrap count++ in 'synchronized(this) { count++; }' (or make increment() itself synchronized). This forces every increment to run as a mutually exclusive unit — no thread can read count while another is mid-update — so all 2000 increments are applied and the final count is reliably 2000.
class Shared {
private int x = 0;
synchronized void inc() { x++; }
int getUnsafe() { return x; } // NOT synchronized
}class Counter {
int count = 0;
void increment() { count++; }
}