Synchronization in Java: Monitors, Race Conditions and Runnable Examples
Learn why Java threads lose updates, what each synchronized form locks, and how to build safe counters, inventory checks and condition-waiting code.
KnowledgeGate Team
Exam prep & CS education

Two threads can execute correct-looking Java code and still corrupt a shared result because value++ is a read, modify and write sequence, not one indivisible action. The runnable counter reaches 100000, and the OneSlot example shows how wait() and notifyAll() coordinate through one monitor.
Why shared state needs synchronization
Start with counter = 41. T1 reads 41; before it writes, T2 also reads 41. Both compute 42, then each writes 42. The final value is 42, although two increments should produce 43. This timing-dependent result is a race condition.
The critical section reads or changes shared mutable state. Mutual exclusion lets only one thread enter it at a time. Atomicity keeps the protected operation indivisible, while visibility lets a later holder of the same monitor see writes completed before its release.
Process Synchronization and Semaphores explains the adjacent operating-system model. Java applies the idea through monitors; synchronized is not a semaphore.
What Java's synchronized keyword locks and guarantees
Every Java object has a monitor. The Oracle Java Language Specification, Chapter 17 specifies that entering synchronized code acquires it and normal or abrupt exit releases it.
The code form determines the monitor:
Form | Monitor acquired |
|---|---|
| The current instance, |
| The exact object referenced by |
| The class object, such as |
Threads coordinate only through the same monitor. An unlock of monitor m happens-before every later lock of m. Thus writes in the first critical section are visible when the next thread acquires m. A different lock cannot protect that state.
Fully worked runnable example: four threads and one safe counter
The unsafe value++ means: read, add 1, write. In the earlier schedule, both threads read 41 and write 42, losing the expected 43. An unsynchronized stress test may print less than 100,000, but no particular wrong result is guaranteed.
class SafeCounter {
private int value = 0;
public synchronized void increment() {
value++;
}
public synchronized int getValue() {
return value;
}
}
public class SyncCounterDemo {
public static void main(String[] args) throws InterruptedException {
SafeCounter counter = new SafeCounter();
Runnable job = () -> {
for (int i = 0; i < 25_000; i++) {
counter.increment();
}
};
Thread[] workers = {
new Thread(job, "T1"), new Thread(job, "T2"),
new Thread(job, "T3"), new Thread(job, "T4")
};
for (Thread worker : workers) worker.start();
for (Thread worker : workers) worker.join();
System.out.println(counter.getValue());
}
}The check is 4 x 25,000 = 100,000. Every worker uses the same SafeCounter, so all contend for its this monitor. The four join() calls finish before the synchronized getter reads. The program prints 100000.

Synchronized method, synchronized block and static lock scope
Use an instance synchronized method when a whole operation maintains an object's invariant, a block for a smaller critical section or private lock, and a static synchronized method only for class-level state. Smaller scope can reduce waiting, but checking outside and updating inside the lock can break correctness.
class Inventory {
private int stock = 10;
private final Object stockLock = new Object();
boolean reserve(int units) {
synchronized (stockLock) {
if (stock < units) return false;
stock -= units;
return true;
}
}
}If order A enters first, reserve(7) changes stock from 10 to 3 and returns true. Order B then tries reserve(6), gets false, and leaves stock at 3.
Code form | Monitor | Coordinates with |
|---|---|---|
Instance synchronized method on |
| Code locking that same |
Block using | That private | Code using that exact lock object |
Static synchronized method |
| Code locking |
shopA and shopB have different stockLock objects, so calls may overlap. Static synchronization does not exclude code locking shopA or its stockLock.
Visibility, volatile, atomic classes and condition waiting
Use synchronization for a compound check and update when every participant uses one lock. A volatile int count is visible but leaves count++ non-atomic. AtomicInteger.incrementAndGet() suits a single counter; four workers making 25,000 calls each produce 100,000.
Condition waiting uses a loop:
class OneSlot {
private String item;
public synchronized String take() throws InterruptedException {
while (item == null) wait();
String result = item;
item = null;
notifyAll();
return result;
}
public synchronized void put(String value) throws InterruptedException {
while (item != null) wait();
item = value;
notifyAll();
}
}The slot starts null. Consumer C1 owns the monitor, finds no item, calls wait(), and releases it. Producer P1 acquires it, stores "JOB-17", calls notifyAll(), and exits. C1 reacquires the monitor, returns "JOB-17", and resets the slot to null.
Under Chapter 17's wait-set rules, wait() releases the owned monitor; Thread.sleep() does not. notifyAll() wakes waiters, but each must reacquire the monitor. The while loop rechecks the condition after wake-up.

Common synchronization errors and deadlock prevention
Mistake | What goes wrong | What to do instead |
|---|---|---|
Locking | Calls get different monitors | Keep one stable private lock |
Protecting one field with different locks | Critical sections overlap | Use one locking rule |
Locking a public object or interned | Unrelated code can contend | Use a private final lock |
Calling |
| Call it inside synchronization on that object |
Assuming | Threads remain blocked | Use condition waiting |
Mixing static and instance synchronization | Class and instance monitors differ | Use one lock identity |
Holding a broad lock during slow I/O | Contention grows | Move I/O outside the critical section |
Take accounts 101 and 202, each with balance 1,000. T1 transfers 100 from 101 to 202, while T2 transfers 200 back. With source-first locking, T1 can hold 101 waiting for 202, while T2 holds 202 waiting for 101.
Fix this with a global order: both transfers acquire the lower ID first, so both attempt 101 before 202. Check and update both balances while holding both locks, then release them on block exit. Keep critical sections short and avoid unknown callbacks while locked. Synchronization alone cannot prevent deadlock, starvation, or poor throughput.
How interviews and coding tests check synchronization
Typical tasks ask you to trace a lost update, identify a shared monitor, compare lock types, choose among synchronized, volatile, and AtomicInteger, spot IllegalMonitorStateException, justify while around wait(), or repair deadlock.
Four quick checks capture the common traps:
Synchronized instance calls on two different
SafeCounterobjects may overlap because their monitors differ.A static synchronized method and an instance synchronized method may overlap because they lock
SafeCounter.classand one instance.Changing the field to
volatile int valuedoes not makevalue++atomic.Starting with stock
10,reserve(7)thenreserve(6)returnstrue, thenfalse, with3remaining.
Use Process Synchronization MCQs for wider critical-section practice. It teaches operating-system primitives; Java monitors instead coordinate through object lock identity. For broader Java and placement practice, browse Coding and DSA.
Synchronization in Java: the short version and next step
Use this checklist:
Identify the shared mutable state.
Choose one stable lock.
Keep the full invariant inside the critical section.
Distinguish instance, block, and class locks.
Put
wait()conditions insidewhileloops.Acquire multiple locks in one global order.
The result remains 4 x 25,000 = 100,000. Java Course: Concepts, MCQs and Coding Questions is the structured next step for Java concepts and practice. Later, DSA using Java: Placement Preparation Course applies Java to data structures and interview problems.
Now change the counter to 5 threads with 12,000 increments each. Predict 5 x 12,000 = 60,000, run it, then remove synchronized only to observe that a wrong value is possible but not guaranteed on every run. Restore synchronization and confirm the stable output 60000.
Keep learning

Abstraction and Interfaces in Java: Abstract Classes, Contracts and Runnable Examples
Learn when Java needs an abstract class, when it needs an interface, and how both work together in one runnable charge-calculation program with exact outputs.

Wrapper Classes and Autoboxing in Java: Worked Examples and Null Traps
Learn why Java wrapper classes exist, how boxing and unboxing work, and where nulls, reference identity, and overload selection create surprises.

Java Streams API Tutorial: Filter, Map, Reduce and Collect with Runnable Examples
Learn Java streams from the source-to-terminal mental model, then trace operations, reductions and collectors through runnable examples and practice tasks.

Optional in Java: Null-Safe Patterns with Runnable Examples
Learn Java Optional from creation to stream pipelines through one user lookup. Trace present and empty paths, compare fallbacks, and repair common mistakes.