In a multithreaded program, multiple threads run at the same time and share access to memory. If two threads try to write to the same variable at the exact same split-second, they will clash, causing index errors or corrupt data. Java solves this using the synchronized keyword to lock critical sections of code.

In this guide, we will translate how synchronized monitors work using a simple telephone booth analogy and walk through its program execution path.

Illustration of threads waiting outside a glass booth representing object monitor locking
Real-World Analogy: The Public Telephone Booth

Imagine a busy street corner with a **public telephone booth**. The booth is big enough for exactly **one person** to enter at a time.

  • The Lock (`synchronized`): When someone enters the booth to make a call, they close and lock the door. This locked door is the "monitor lock." While they are talking inside, no one else can enter.
  • The Queue (Waiting Threads): If other people arrive wanting to make a call, they must form a line outside the booth and wait.
  • Releasing the Lock: Once the caller hangs up, unlocks the door, and exits, the lock is released, and the next person in line can enter.
  • Unsynchronized Code: If someone just wants to read the public clock mounted on the outside of the telephone booth, they do not need to enter. They can read it immediately without waiting in line!

Walkthrough of the Main Method Scenario

Let's trace how the program coordinates execution step-by-step from the entry point:

Step 1: Starting the Competitor Threads

The program enters the main method, constructs a shared instances coordinator, and spawns three independent threads:

UnderstandingSynchronized us = new UnderstandingSynchronized();
Thread t1 = new Thread(() -> us.count());
Thread t2 = new Thread(() -> us.count());
Thread t3 = new Thread(() -> System.out.println("Hello "));

Threads t1 and t2 are competing to enter the synchronized count() method, while t3 is unsynchronized and simply prints a message immediately.

Step 2: Entering the Synchronized Method

When the threads are started simultaneously:

  • Thread t3 (Unsynchronized): It bypasses all locks, runs its task immediately, prints "Hello " to the console, and exits. It did not have to wait for anything.
  • Thread t1: It reaches the count() method first. Since the method is synchronized, t1 acquires the monitor lock for the shared object `us`. It logs: "Inside Thread Thread-0".
  • Thread t2: It reaches the method next. Because t1 currently holds the monitor lock of `us`, t2 is put to sleep by the JVM and placed in the wait queue outside the method gate.
Step 3: Block synchronization and Hand-off

Inside the method body, another lock is checked:

synchronized (lock) {
    System.out.println("Hello World !!!");
}

Even though the string lock is local, it behaves as a block lock. Thread t1 completes this synchronized block, exits the method, and releases the monitor lock of `us`.

Immediately, the JVM wakes up Thread t2 from the queue. t2 locks the monitor, enters the method, logs its thread name, executes the block, and finishes.

Java Implementation

Below is the complete Java code demonstrating method-level and block-level synchronization:

package io.practise.accolite;
 
public class UnderstandingSynchronized {
    public static void main(String[] args) {
 
        UnderstandingSynchronized us = new UnderstandingSynchronized();
 
        Thread t1 = new Thread(() -> us.count());
        Thread t2 = new Thread(() -> us.count());
        Thread t3 = new Thread(() -> System.out.println("Hello "));
 
        t1.start();
        t2.start();
        t3.start();
    }
 
    public synchronized void count()  {
        String lock = "lock";
        System.out.println("Inside Thread " + Thread.currentThread().getName());
        synchronized (lock) {
            System.out.println("Hello World !!!");
        }
    }
}

Conclusion

Java's synchronized keyword provides automatic locking. When declared on a method, it locks the calling instance (this). When used as a block, it locks a specific target object monitor. Understanding the scope of these monitors is crucial to building race-free multithreaded applications without slowing down execution unnecessarily.