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

Updated 26 Sep 20266 min read

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

synchronized void add()

The current instance, this

synchronized (lock) { ... }

The exact object referenced by lock

static synchronized void reset()

The class object, such as Counter.class

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.

java
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.

Timeline of two threads incrementing a counter from 41: the unsafe run both write 42, the synchronized run reaches the expected 43.

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.

java
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 shopA

shopA (this)

Code locking that same shopA instance

Block using shopA.stockLock internally

That private stockLock object

Code using that exact lock object

Static synchronized method

Inventory.class

Code locking Inventory.class

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:

java
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.

Four states showing consumer C1 waiting on the OneSlot monitor until producer P1 stores JOB-17 and calls notifyAll().

Common synchronization errors and deadlock prevention

Mistake

What goes wrong

What to do instead

Locking new Object() inside every call

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 String

Unrelated code can contend

Use a private final lock

Calling wait() without owning the monitor

IllegalMonitorStateException

Call it inside synchronization on that object

Assuming sleep() releases a lock

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:

  1. Synchronized instance calls on two different SafeCounter objects may overlap because their monitors differ.

  2. A static synchronized method and an instance synchronized method may overlap because they lock SafeCounter.class and one instance.

  3. Changing the field to volatile int value does not make value++ atomic.

  4. Starting with stock 10, reserve(7) then reserve(6) returns true, then false, with 3 remaining.

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:

  1. Identify the shared mutable state.

  2. Choose one stable lock.

  3. Keep the full invariant inside the critical section.

  4. Distinguish instance, block, and class locks.

  5. Put wait() conditions inside while loops.

  6. 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.