Chapter 42: Synchronized Blocks and Inter-Thread Communication in Java

Chapter 42: Synchronized Blocks and Inter-Thread Communication in Java

In the world of multi-threaded programming, managing shared resources and synchronizing access to them is crucial to avoid concurrency issues like race conditions. Java provides mechanisms to synchronize blocks of code to ensure thread safety. This chapter delves into Synchronized Blocks in Java, explaining their usage, importance, and the underlying thread synchronization concepts.

The chapter begins with an exploration of the Synchronized Block and its role in controlling access to shared resources. We’ll understand how the synchronized keyword is used to lock specific sections of code, ensuring that only one thread can access them at a time. Additionally, we explore how to track which thread is executing inside or outside the synchronized block.

Through practical examples, we will investigate scenarios where multiple threads interact with different objects, how class-level locks work, and how to ensure that inter-thread communication flows smoothly. Key concepts like wait(), notify(), and notifyAll() will be explained in detail, alongside real-world examples.

This chapter also covers critical aspects of inter-thread communication, focusing on how threads wait for and notify one another. By understanding these mechanisms, you’ll be better equipped to design applications where multiple threads can work concurrently without causing deadlocks or inconsistent states.

As we explore these concepts, we’ll also touch upon important exceptions like the IllegalMonitorStateException and thread life cycles when calling wait() or notify(). By the end of this chapter, you’ll have a solid understanding of synchronizing threads and handling inter-thread communication effectively in Java.


Synchronized Block in Java: A Comprehensive Explanation

The synchronized block in Java is a powerful mechanism used to control access to critical sections of code by multiple threads. It ensures that only one thread at a time can execute a synchronized block of code, providing thread safety.


Theory

  1. Synchronized Block:

    • A synchronized block is a part of a method or a block of code that needs to be executed by only one thread at a time.

    • The keyword synchronized ensures that a thread gets an exclusive lock on the specified object before executing the block.

  2. Locking Mechanism:

    • Each object in Java has a unique intrinsic lock.

    • A thread must acquire this lock to enter a synchronized block.

    • Once the thread exits the block, the lock is released.

  3. Use Cases:

    • Avoiding data inconsistency in multithreaded environments.

    • Ensuring thread-safe operations on shared resources.


Example

Below is a detailed example demonstrating the concept:

Code: Before and After Synchronized Block

// Example of synchronized block in a multithreaded environment

class Display {
    public void wish(String name) {
        // Code before synchronized block
        System.out.println("Thread started: " + name);

        // Writing task in synchronized block
        // -> Here lock is applied for the thread for executing this block
        synchronized (this) {
            for (int i = 1; i <= 5; i++) {
                System.out.print("Good Morning: ");
                try {
                    Thread.sleep(2000); // Simulating time-consuming task
                } catch (InterruptedException e) {
                    System.out.println("Thread interrupted");
                }
                System.out.println(name);
            }
        }

        // Code after synchronized block
        System.out.println("Thread ended: " + name);
    }
}

class MyThread extends Thread {
    Display display; // -> Has-A relationship
    String name;

    MyThread(Display display, String name) {
        this.display = display;
        this.name = name;
    }

    @Override
    public void run() {
        display.wish(name);
    }
}

public class MultithreadingExample {
    public static void main(String[] args) {
        // Creating Display object
        Display display = new Display();

        // Creating MyThread objects and passing arguments
        MyThread t1 = new MyThread(display, "Rohit");
        MyThread t2 = new MyThread(display, "Sachin");

        t1.start();
        t2.start();
    }
}

Output

Thread started: Rohit
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Thread ended: Rohit
Thread started: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Thread ended: Sachin

Explanation of the Example

  1. Class Display:

    • The wish method contains synchronized code.

    • The synchronized block ensures that only one thread can execute the critical section at a time.

  2. Class MyThread:

    • Demonstrates a Has-A relationship with the Display class.

    • Each thread is associated with a Display object and a name.

  3. Main Class:

    • Creates a single Display object shared by two threads.

    • Two threads (t1 and t2) invoke the wish method on the shared Display object.

  4. Execution:

    • Threads t1 and t2 try to execute the synchronized block.

    • Only one thread holds the lock at a time, ensuring sequential execution.


Real-World Analogy

Imagine a single ATM machine shared by multiple users. Only one person can withdraw money at a time (acquiring the lock). Others must wait until the current user finishes and releases the lock.


Diagram

Below is a simple diagram to illustrate the working of a synchronized block:

Thread-1       |  Thread-2       |  Display Object
---------------------------------------------------
Acquire Lock   |                 |  Locked by Thread-1
Execute Block  |  Waiting        |  Synchronized Block in Progress
Release Lock   |                 |  Lock Released
               |  Acquire Lock   |  Locked by Thread-2
               |  Execute Block  |  Synchronized Block in Progress
               |  Release Lock   |  Lock Released

Key Points

  • Code Before and After Synchronized Block:

    • Threads can execute non-critical code simultaneously.

    • Only the critical section (synchronized block) is protected by the lock.

  • Thread Safety:

    • Prevents race conditions and ensures consistency of shared resources.
  • Drawbacks:

    • Can lead to reduced performance due to thread contention.

    • Deadlocks may occur if multiple locks are involved improperly.


Synchronized Block: Knowing Which Thread is Executing Outside the Synchronized Block


Theory

In multithreading, the execution of code outside a synchronized block is not constrained by locks. As a result, multiple threads can execute this part of the code simultaneously, leading to interleaved outputs. The synchronized block ensures that only one thread at a time can access the critical section, but outside this block, threads operate independently.


Key Concepts

  1. Thread Names:

    • Thread.currentThread().getName() retrieves the name of the currently executing thread.

    • This helps identify which thread is active at any given point.

  2. Synchronized Blocks:

    • Locks only the specified section of code.

    • Provides a mechanism for thread-safe operations.

  3. Execution Order:

    • Code outside the synchronized block is executed by any thread without restrictions.

    • Code inside the block is executed sequentially by acquiring the lock on the object.


Real-World Analogy

Imagine two bank tellers processing transactions. The area outside the teller's counter represents the code outside the synchronized block, where customers (threads) freely interact with other services. The counter represents the synchronized block, where only one customer can be served at a time.


Code Example

Below is the Java code example illustrating the concept:

class Display {
    public void wish(String name) {
        // Code before synchronized block
        System.out.println("Thread which is getting lock is: " + Thread.currentThread().getName()); // Line-1

        // Writing task in synchronized block
        synchronized (this) {
            for (int i = 1; i <= 5; i++) {
                System.out.print("Good Morning: ");
                try {
                    Thread.sleep(2000); // Simulating time-consuming task
                } catch (InterruptedException e) {
                    System.out.println("Thread interrupted");
                }
                System.out.println(name);
            }
        }
    }
}

class MyThread extends Thread {
    Display d; // Has-A relationship
    String name;

    MyThread(Display d, String name) {
        this.d = d;
        this.name = name;
    }

    @Override
    public void run() {
        d.wish(name);
    }
}

public class MultithreadingExample {
    public static void main(String[] args) {
        // Creating display object
        Display d = new Display();

        // Creating MyThread objects and passing arguments
        MyThread t1 = new MyThread(d, "Rohit");
        MyThread t2 = new MyThread(d, "Sachin");

        t1.setName("Rohit Thread");
        t2.setName("Sachin Thread");

        t1.start();
        t2.start();
    }
}

Output

Thread which is getting lock is: Sachin Thread
Thread which is getting lock is: Rohit Thread
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit

Explanation

  1. Code Before Synchronized Block:

    • Both threads execute Line-1 concurrently because it is outside the synchronized block.

    • This results in the thread names being printed in an unpredictable order.

  2. Code Inside Synchronized Block:

    • The thread holding the lock executes the for loop inside the synchronized block.

    • Other threads wait until the lock is released.

  3. Output Behavior:

    • "Thread which is getting lock is:" statements are interleaved.

    • "Good Morning:" messages for each thread are sequential, reflecting the synchronized block.


Diagram

Execution Timeline

Time  →     Thread-1 (Rohit Thread)      Thread-2 (Sachin Thread)
------------------------------------------------------------
T1    →  Executes Line-1 (Outside)   Executes Line-1 (Outside)
T2    →              Lock Acquired         Waiting
T3    →  Executes Synchronized Block      Waiting
T4    →  Releasing Lock                   Lock Acquired
T5    →  Waiting                          Executes Synchronized Block

Real-World Application

This pattern is common in scenarios where multiple threads need to log information or perform non-critical operations outside synchronized sections, such as:

  • Logging thread activity in a server application.

  • Printing debug information in a concurrent system.


Conclusion

The example effectively demonstrates how threads behave outside and inside a synchronized block. The interleaving of thread outputs outside the block highlights the need for synchronization in critical sections. This understanding is crucial for designing efficient and thread-safe multithreaded applications.

3.Synchronized Blocks: Knowing Which Thread is Getting and Releasing the Lock


Theory

In multithreaded applications, synchronized blocks ensure that only one thread can execute a critical section of code at a time by acquiring an object-level lock. This example demonstrates how to identify:

  1. The thread that acquires the lock.

  2. The thread that releases the lock after completing the synchronized block.

By understanding lock acquisition and release, developers can monitor thread behavior and debug concurrency issues more effectively.


Key Concepts

  1. Lock Mechanism:

    • The synchronized keyword applies a lock to the current object (this).

    • Only one thread can hold the lock at any given time, ensuring exclusive access to the synchronized block.

  2. Thread Identification:

    • Thread.currentThread().getName() is used to display the name of the thread that acquires or releases the lock.
  3. Execution Flow:

    • Threads execute code outside the synchronized block without restrictions.

    • Inside the block, threads wait for the lock, execute the critical section, and then release the lock.


Real-World Analogy

Imagine a single-chair barber shop:

  • Only one customer (thread) can sit in the barber's chair (critical section) at a time.

  • Other customers must wait for the barber (lock) to finish before they can sit.

  • The barber announces (logs) which customer is being served (lock acquired) and when the chair is free (lock released).


Code Example

Below is the Java code illustrating lock acquisition and release:

class Display {
    public void wish(String name) {
        // Writing task in synchronized block
        synchronized (this) {
            // Knowing thread which is getting lock
            System.out.println("Thread which is getting lock is: " + Thread.currentThread().getName());

            for (int i = 1; i <= 5; i++) {
                System.out.print("Good Morning: ");
                try {
                    Thread.sleep(2000); // Simulating a time-consuming task
                } catch (InterruptedException e) {
                    System.out.println("Thread interrupted");
                }
                System.out.println(name);
            }

            // Knowing thread which is releasing lock
            System.out.println("Thread which is releasing lock is: " + Thread.currentThread().getName());
        }
    }
}

class MyThread extends Thread {
    Display d; // Has-A relationship
    String name;

    MyThread(Display d, String name) {
        this.d = d;
        this.name = name;
    }

    @Override
    public void run() {
        d.wish(name);
    }
}

public class MultithreadingExample {
    public static void main(String[] args) {
        // Creating Display object
        Display d = new Display();

        // Creating MyThread objects and passing arguments
        MyThread t1 = new MyThread(d, "Rohit");
        MyThread t2 = new MyThread(d, "Sachin");

        t1.setName("Rohit Thread");
        t2.setName("Sachin Thread");

        t1.start();
        t2.start();
    }
}

Output

Thread which is getting lock is: Rohit Thread
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Thread which is releasing lock is: Rohit Thread
Thread which is getting lock is: Sachin Thread
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Thread which is releasing lock is: Sachin Thread

Explanation

  1. Thread Lock Acquisition:

    • The first thread to enter the synchronized block acquires the lock and logs the message:
      "Thread which is getting lock is: <thread name>".
  2. Critical Section Execution:

    • The thread executes the for loop, printing "Good Morning" messages along with the name.
  3. Thread Lock Release:

    • After completing the block, the thread releases the lock and logs:
      "Thread which is releasing lock is: <thread name>".
  4. Thread Scheduling:

    • The second thread waits for the lock to be released, acquires it, and repeats the process.

Diagram

Thread Execution Flow

Thread 1: Rohit Thread          Thread 2: Sachin Thread
--------------------------------------------------------
Acquires Lock                   Waiting
Logs "Getting Lock"             
Executes Loop (Critical Section)
Logs "Releasing Lock"           
Releases Lock                   Acquires Lock
                                Logs "Getting Lock"
                                Executes Loop (Critical Section)
                                Logs "Releasing Lock"
                                Releases Lock

Real-World Application

  1. Banking System:

    • Logging which teller (thread) is processing a customer (acquiring lock) and when they finish (releasing lock).
  2. Logging Systems:

    • Debugging concurrent systems by tracking thread activity during critical operations.
  3. Multithreaded File Access:

    • Ensuring synchronized access to shared file resources while logging thread operations.

Conclusion

This example highlights the importance of synchronized blocks in controlling thread access and logging lock acquisition and release. It demonstrates how thread-safe operations can be achieved with proper synchronization, providing insights into debugging and managing multithreaded environments.

Example 4: Multiple Threads Operating on Multiple Objects


1. Theory

In multithreaded programming, object-level locks are used to synchronize a block of code within a single object. When multiple threads operate on separate objects, each thread gets its own lock for its respective object. Consequently, synchronization only applies to the critical section of a specific object, and threads operating on different objects can execute concurrently.


2. Key Concepts

  1. Object-Level Lock:

    • When synchronized(this) is used, the lock applies to the current object (this).

    • Each object in Java has a unique intrinsic lock.

  2. Multiple Objects:

    • If multiple threads operate on multiple objects, each object has its own lock.

    • Synchronization is limited to individual objects, resulting in irregular output.

  3. Concurrency:

    • Threads acting on different objects can interleave their operations since their locks are independent.

3. Real-World Analogy

Imagine two independent ATM machines (objects). Multiple customers (threads) can operate concurrently on different machines. The synchronization applies only to each individual machine and not across machines, leading to potential overlap in operations between the machines.


4. Code Example

Below is the example code illustrating the behavior of multiple threads operating on multiple objects:

class Display {
    public void wish(String name) {
        synchronized (this) {
            // Knowing which thread gets the lock
            System.out.println("Thread which is getting lock is: " + Thread.currentThread().getName());

            for (int i = 1; i <= 5; i++) {
                System.out.print("Good Morning: ");
                try {
                    Thread.sleep(2000); // Simulating time-consuming task
                } catch (InterruptedException e) {
                    System.out.println("Thread interrupted");
                }
                System.out.println(name);
            }

            // Knowing which thread releases the lock
            System.out.println("Thread which is releasing lock is: " + Thread.currentThread().getName());
        }
    }
}

class MyThread extends Thread {
    Display d; // Has-A relationship
    String name;

    MyThread(Display d, String name) {
        this.d = d;
        this.name = name;
    }

    @Override
    public void run() {
        d.wish(name);
    }
}

public class MultithreadingExample {
    public static void main(String[] args) {
        // Creating multiple Display objects
        Display d1 = new Display();
        Display d2 = new Display();

        // Creating MyThread objects and passing arguments
        MyThread t1 = new MyThread(d1, "Rohit");
        MyThread t2 = new MyThread(d2, "Sachin");

        t1.setName("Rohit Thread");
        t2.setName("Sachin Thread");

        t1.start();
        t2.start();
    }
}

5. Output

Thread which is getting lock is: Sachin Thread
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Thread which is releasing lock is: Sachin Thread
Thread which is getting lock is: Rohit Thread
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Thread which is releasing lock is: Rohit Thread

6. Explanation

  1. Thread Behavior:

    • Each thread acquires the lock of its respective object (d1 or d2).

    • Threads execute independently as they operate on different objects.

  2. Irregular Output:

    • Since there is no shared synchronization across objects, outputs from the threads interleave.
  3. Lock Acquisition and Release:

    • Each thread logs when it acquires or releases the lock on its object.
  4. Concurrency:

    • Despite synchronization within each object, multiple threads can execute in parallel on different objects.

7. Diagram

Thread Execution Flow

Thread 1: Rohit Thread          Thread 2: Sachin Thread
--------------------------------------------------------
Acquires Lock (d1)              Acquires Lock (d2)
Logs "Getting Lock" (d1)        Logs "Getting Lock" (d2)
Executes Loop (Critical Section on d1)  
                                Executes Loop (Critical Section on d2)
Logs "Releasing Lock" (d1)      Logs "Releasing Lock" (d2)
Releases Lock (d1)              Releases Lock (d2)

8. Real-World Applications

  1. Independent Resource Access:

    • Multiple threads operating on separate database connections or file handles.
  2. Parallel Processing:

    • Independent tasks executed by threads working on separate instances of a resource.
  3. UI Operations:

    • Separate threads handling different UI components concurrently.

9. Key Observations

  • Synchronization is object-specific, not thread-specific.

  • Irregular outputs occur when threads act on different objects simultaneously.

  • This behavior is expected and is not a threading issue, as the locks are independent.


10. Conclusion

This example illustrates how synchronized blocks behave when multiple threads operate on multiple objects. Understanding this behavior is crucial for designing systems where independent operations occur concurrently. By leveraging object-level locks, developers can ensure thread safety while allowing parallelism across independent resources.

Example 5: Class-Level Lock


1. Theory

In Java, a class-level lock is used to synchronize a block of code or method at the class level rather than on a specific object. This ensures that only one thread can execute the synchronized block for the class, regardless of the number of instances of that class. Class-level locks are associated with the Class object of the class.


2. Key Concepts

  1. Class-Level Lock:

    • Applied using synchronized(Display.class) for the Display class in this example.

    • Ensures mutual exclusion across all instances of the class.

  2. Thread Behavior:

    • If one thread acquires the class-level lock, no other thread can enter any synchronized block that uses the same class-level lock, even if operating on a different object.
  3. Regular Output:

    • Since threads contend for the same lock at the class level, they execute the synchronized block sequentially, resulting in a regular output.
  4. Lock Restrictions:

    • Locks apply only to objects or class types.

    • Locking primitive types (e.g., int, float) is invalid and results in a compile-time error.


3. Real-World Analogy

Imagine a meeting room in an office that is accessible only when a specific master key (class-level lock) is available. Even if multiple departments (objects) have meetings scheduled, only one department can access the room at a time because the key is shared among all.


4. Code Example

Below is the Java code demonstrating class-level locking:

class Display {
    public void wish(String name) {
        // Applying class-level lock
        synchronized (Display.class) {
            // Knowing which thread gets the lock
            System.out.println("Thread which is getting lock is: " + Thread.currentThread().getName());
            for (int i = 1; i <= 5; i++) {
                System.out.print("Good Morning: ");
                try {
                    Thread.sleep(2000); // Simulating a time-consuming task
                } catch (InterruptedException e) {
                    System.out.println("Thread interrupted");
                }
                System.out.println(name);
            }
            // Knowing which thread releases the lock
            System.out.println("Thread which is releasing lock is: " + Thread.currentThread().getName());
        }
    }
}

class MyThread extends Thread {
    Display d; // Has-A relationship
    String name;

    MyThread(Display d, String name) {
        this.d = d;
        this.name = name;
    }

    @Override
    public void run() {
        d.wish(name);
    }
}

public class MultithreadingExample {
    public static void main(String[] args) {
        // Creating multiple Display objects
        Display d1 = new Display();
        Display d2 = new Display();

        // Creating MyThread objects and passing arguments
        MyThread t1 = new MyThread(d1, "Rohit");
        MyThread t2 = new MyThread(d2, "Sachin");

        t1.setName("Rohit Thread");
        t2.setName("Sachin Thread");

        t1.start();
        t2.start();
    }
}

5. Output

Thread which is getting lock is: Rohit Thread
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Thread which is releasing lock is: Rohit Thread
Thread which is getting lock is: Sachin Thread
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Thread which is releasing lock is: Sachin Thread

6. Explanation

  1. Class-Level Lock:

    • The synchronized(Display.class) block applies the lock to the Display class.

    • Even though there are two Display objects (d1 and d2), both threads compete for the same class-level lock.

  2. Thread Execution:

    • One thread acquires the class-level lock and executes the block.

    • Other threads must wait for the lock to be released, ensuring sequential execution.

  3. Output Regularity:

    • Because the lock is at the class level, thread outputs do not overlap.

    • Each thread completes its execution before the next thread begins.

  4. Compile-Time Errors:

    • Attempting to use locks on primitive types like int results in errors, as locks are applicable only to objects or classes.

7. Diagram

Thread Execution with Class-Level Lock

Thread 1: Rohit Thread         Thread 2: Sachin Thread
--------------------------------------------------------
Acquires Lock (Display.class)  Waiting for Lock
Logs "Getting Lock"
Executes Loop (Critical Section)
Logs "Releasing Lock"
Releases Lock                  Acquires Lock (Display.class)
                               Logs "Getting Lock"
                               Executes Loop (Critical Section)
                               Logs "Releasing Lock"
                               Releases Lock

8. Real-World Applications

  1. Resource Management:

    • Synchronizing access to a shared static resource like a database connection pool.
  2. Logging Systems:

    • Ensuring that log entries are written sequentially, even when multiple threads attempt to log simultaneously.
  3. Singleton Pattern:

    • Preventing multiple threads from creating separate instances of a singleton class.

9. Key Observations

  1. Class-Level Synchronization:

    • The lock ensures that only one thread can execute the synchronized block across all instances of the class.
  2. Independence of Objects:

    • Class-level locking overrides the independence of object-level locking.
  3. Thread Safety:

    • Guarantees thread-safe execution of critical sections shared across instances.

10. Conclusion

Class-level locks are crucial for scenarios requiring mutual exclusion across all instances of a class. By applying synchronization at the class level, developers can enforce sequential access to shared resources, ensuring consistency and preventing race conditions. This example clearly illustrates the power and utility of class-level synchronization in multithreaded Java programs.

Scenario to Understand Inter-Thread Communication


1. Theory

Inter-thread communication in Java, often referred to as cooperation, is a mechanism that allows threads to communicate and coordinate their actions while working with shared resources. This is crucial in multithreading environments to ensure that threads use resources efficiently without busy waiting, which degrades performance.


2. Key Concepts

  1. Busy Waiting Problem:

    • A thread repeatedly checks for a condition or resource availability, consuming CPU cycles unnecessarily.

    • In this example, Person-1 (Thread-1) constantly checks the postbox, leading to inefficiency.

  2. Inter-Thread Communication:

    • Enables threads to notify each other about changes in the state of shared resources.

    • Implemented using methods from the Object class:

      • wait(): Causes the current thread to wait until another thread invokes notify() or notifyAll() on the same object.

      • notify(): Wakes up a single thread that is waiting on the object's monitor.

      • notifyAll(): Wakes up all threads waiting on the object's monitor.

  3. Real-World Analogy:

    • Person-1 (Thread-1) waits for a notification (e.g., a bell or message) from the Postman (Thread-2) when a letter is delivered. This prevents Person-1 from repeatedly checking the postbox unnecessarily.

3. Example Explanation

Scenario Setup

  1. Person-1 is Thread-1.

  2. Postman is Thread-2.

  3. Postbox is the shared resource enabling inter-thread communication.

Steps:

  1. Thread-1 waits for a notification about the letter.

  2. Thread-2 notifies Thread-1 when the letter is delivered.

  3. This eliminates the need for Thread-1 to continuously check for the letter.


4. Java Code Example

Below is the Java implementation for the scenario:

class Postbox {
    private boolean letterAvailable = false;

    public synchronized void waitForLetter() {
        while (!letterAvailable) {
            try {
                System.out.println(Thread.currentThread().getName() + " is waiting for the letter.");
                wait(); // Person-1 enters waiting state
            } catch (InterruptedException e) {
                System.out.println("Thread interrupted");
            }
        }
        System.out.println(Thread.currentThread().getName() + " got the letter and is reading it.");
        letterAvailable = false; // Resetting the state for next use
    }

    public synchronized void deliverLetter() {
        System.out.println(Thread.currentThread().getName() + " delivered the letter.");
        letterAvailable = true; // Letter is now available
        notify(); // Notify the waiting thread
    }
}

class Person1 extends Thread {
    private final Postbox postbox;

    public Person1(Postbox postbox) {
        this.postbox = postbox;
    }

    @Override
    public void run() {
        postbox.waitForLetter(); // Wait for the letter
    }
}

class Postman extends Thread {
    private final Postbox postbox;

    public Postman(Postbox postbox) {
        this.postbox = postbox;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(5000); // Simulating delay before delivering the letter
        } catch (InterruptedException e) {
            System.out.println("Thread interrupted");
        }
        postbox.deliverLetter(); // Deliver the letter
    }
}

public class InterThreadCommunicationExample {
    public static void main(String[] args) {
        Postbox postbox = new Postbox();
        Person1 person1 = new Person1(postbox); // Thread-1
        Postman postman = new Postman(postbox); // Thread-2

        person1.setName("Person-1");
        postman.setName("Postman");

        person1.start();
        postman.start();
    }
}

5. Output

Person-1 is waiting for the letter.
Postman delivered the letter.
Person-1 got the letter and is reading it.

6. Explanation of the Code

  1. Class Postbox:

    • Acts as the shared resource.

    • Maintains a flag (letterAvailable) to indicate if a letter is present.

    • Uses wait() and notify() for inter-thread communication.

  2. Thread Person1 (Thread-1):

    • Represents the behavior of Person-1.

    • Calls waitForLetter() to wait for the letter.

  3. Thread Postman (Thread-2):

    • Represents the behavior of the Postman.

    • Calls deliverLetter() to notify the waiting thread.

  4. Execution:

    • Thread-1 starts and waits for the letter.

    • Thread-2 delivers the letter and notifies Thread-1.

    • Thread-1 resumes and processes the letter.


7. Diagram

Inter-Thread Communication Workflow

Person-1 (Thread-1)             Postbox (Shared Resource)              Postman (Thread-2)
----------------------------------------------------------------------------------------
Calls waitForLetter()           Enters waiting state
                                ---------------------------
                                Letter not available
                                ---------------------------
                                (Thread-1 is waiting)
                                                                   Calls deliverLetter()
                                                                   ---------------------
                                                                   Letter available
                                                                   Notifies Thread-1
                                ---------------------------
                                Letter available
                                ---------------------------
Resumes and reads letter

8. Real-World Applications

  1. Producer-Consumer Problem:

    • Producers add items to a buffer, and consumers consume them. Inter-thread communication ensures proper synchronization.
  2. Event Notification Systems:

    • Notifying workers when a task is added to a queue.
  3. Thread Coordination:

    • Threads waiting for a signal or condition to proceed.

9. Key Observations

  1. Efficient Resource Usage:

    • Eliminates busy waiting by putting threads in a waiting state.
  2. Thread Synchronization:

    • Ensures threads coordinate their activities using shared resources effectively.
  3. Avoiding Deadlocks:

    • Proper use of wait(), notify(), and notifyAll() ensures smooth communication.

10. Conclusion

Inter-thread communication allows threads to work together efficiently, avoiding performance issues like busy waiting. By understanding and applying methods like wait(), notify(), and notifyAll(), developers can build responsive and resource-efficient multithreaded applications. This example demonstrates the concept using a relatable analogy and practical implementation.

Inter-Thread Communication: Understanding wait(), notify(), and notifyAll()


1. Theory

Inter-thread communication enables threads to coordinate their actions and share resources efficiently. The wait(), notify(), and notifyAll() methods from the Object class provide a structured mechanism for threads to signal and wait for changes in shared resource states.

These methods must be called within synchronized blocks or synchronized methods because they rely on the monitor (lock) associated with an object.


2. Key Concepts: The wait() Method

  1. Purpose:

    • wait() is used by a thread to pause execution and release the lock on an object until another thread notifies it.
  2. Behavior:

    • The thread that calls wait() enters the waiting state.

    • The lock held by the thread is released, allowing other threads to access the synchronized block or method.

  3. Conditions:

    • wait() must be called by a thread that holds the lock of the object.

    • If called outside a synchronized block or method, a java.lang.IllegalMonitorStateException is thrown.

  4. Advantages:

    • Prevents busy waiting, where threads repeatedly check for conditions.

    • Enables efficient resource sharing by allowing threads to sleep until a specific condition is met.


3. Real-World Analogy

Imagine two friends:

  • Rohit is waiting for a package from the delivery service.

  • Instead of constantly checking for the package, Rohit goes to sleep (calls wait()).

  • The delivery person delivers the package and rings the doorbell (calls notify()).

  • Upon hearing the bell, Rohit wakes up (resumes execution) and collects the package.


4. Java Example: Using wait()

Below is an example demonstrating the use of wait():

class SharedResource {
    private boolean updated = false;

    public synchronized void waitForUpdate() {
        while (!updated) {
            try {
                System.out.println(Thread.currentThread().getName() + " is waiting for update...");
                wait(); // Thread enters waiting state and releases the lock
            } catch (InterruptedException e) {
                System.out.println("Thread interrupted");
            }
        }
        System.out.println(Thread.currentThread().getName() + " received the update!");
    }

    public synchronized void updateResource() {
        System.out.println(Thread.currentThread().getName() + " is updating the resource...");
        updated = true;
        notify(); // Notify one waiting thread
    }
}

class WaitingThread extends Thread {
    private final SharedResource resource;

    public WaitingThread(SharedResource resource) {
        this.resource = resource;
    }

    @Override
    public void run() {
        resource.waitForUpdate();
    }
}

class NotifyingThread extends Thread {
    private final SharedResource resource;

    public NotifyingThread(SharedResource resource) {
        this.resource = resource;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(3000); // Simulate some work
        } catch (InterruptedException e) {
            System.out.println("Thread interrupted");
        }
        resource.updateResource();
    }
}

public class WaitExample {
    public static void main(String[] args) {
        SharedResource resource = new SharedResource();

        WaitingThread t1 = new WaitingThread(resource); // Thread waiting for update
        NotifyingThread t2 = new NotifyingThread(resource); // Thread performing the update

        t1.setName("Rohit");
        t2.setName("Notifier");

        t1.start();
        t2.start();
    }
}

5. Output

Rohit is waiting for update...
Notifier is updating the resource...
Rohit received the update!

6. Explanation

  1. Shared Resource:

    • Acts as the medium of communication between threads.

    • Maintains a flag (updated) to track the state.

  2. Thread Behavior:

    • Thread t1 (Rohit) calls wait() and enters the waiting state, releasing the lock.

    • Thread t2 (Notifier) acquires the lock, updates the resource, and calls notify().

  3. Notification:

    • The notify() call wakes up Thread t1, which resumes execution after acquiring the lock.
  4. Efficient Coordination:

    • Threads do not waste CPU cycles in busy waiting.

    • Execution is coordinated through the shared resource.


7. Diagram

Thread States with wait() and notify()

Thread: Rohit                    SharedResource                  Thread: Notifier
----------------------------------------------------------------------------------
Calls waitForUpdate()            Enters synchronized block
Enters waiting state             --------------------------------
                                 updated = false
                                 --------------------------------
                                 Releases lock
                                                                  Sleeps for 3 seconds
                                                                  Calls updateResource()
                                 --------------------------------
                                 updated = true
                                 Calls notify()
                                 --------------------------------
Wakes up and acquires lock
Continues execution

8. Real-World Applications

  1. Producer-Consumer Problem:

    • Producers add items to a queue, and consumers process them. wait() ensures consumers pause until producers add items.
  2. Event Notification:

    • Threads wait for specific events, e.g., a file upload or task completion.
  3. Task Queues:

    • Workers wait for tasks in a queue and are notified when a task is available.

9. Key Observations

  1. Thread Coordination:

    • The wait() method is essential for efficient thread communication.

    • Threads must hold the object's lock before calling wait().

  2. Release of Lock:

    • Calling wait() immediately releases the lock, allowing other threads to proceed.
  3. Synchronization:

    • All communication methods (wait(), notify(), notifyAll()) must occur within synchronized blocks or methods.

10. Conclusion

The wait() method is a cornerstone of inter-thread communication in Java. By enabling threads to pause execution and release locks, it ensures efficient and synchronized resource sharing. Together with notify() and notifyAll(), it forms the foundation of cooperative threading in Java, eliminating the inefficiencies of busy waiting.

Inter-Thread Communication: Understanding notify() and notifyAll()


1. Theory

In Java, notify() and notifyAll() are key methods used in inter-thread communication. These methods enable threads to signal other threads waiting on a shared resource that an update has occurred or a condition has been met. They are critical for coordinating thread actions and ensuring proper resource utilization.


2. Key Concepts

  1. notify():

    • Wakes up one waiting thread that is in the waiting state on the same object's monitor.

    • The thread that is woken up competes for the lock and resumes execution once it acquires the lock.

  2. notifyAll():

    • Wakes up all threads waiting on the same object's monitor.

    • All woken threads compete for the lock, and only one thread proceeds at a time after acquiring the lock.

  3. Ownership:

    • A thread must hold the lock on the object to call notify() or notifyAll().

    • These methods must be invoked within a synchronized block or method. Otherwise, a java.lang.IllegalMonitorStateException will be thrown.

  4. Lock Behavior:

    • The thread calling notify() or notifyAll() may not immediately release the lock.

    • The lock is released only when the thread exits the synchronized block or method.

  5. Comparison with wait():

    • While wait() puts a thread into the waiting state, notify() or notifyAll() signals waiting threads to wake up.

3. Real-World Analogy

Imagine a customer (Thread-1) waiting in a queue to collect a parcel. The parcel delivery clerk (Thread-2) processes the parcel and rings a bell (calls notify()), signaling the customer to proceed and collect their parcel. If multiple customers are waiting, the clerk can announce to all customers (calls notifyAll()), allowing them to compete for service.


4. Java Example: Using notify() and notifyAll()

Below is a Java implementation demonstrating the use of notify() and notifyAll():

class SharedResource {
    private boolean updated = false;

    public synchronized void waitForUpdate() {
        while (!updated) {
            try {
                System.out.println(Thread.currentThread().getName() + " is waiting for an update...");
                wait(); // Thread enters waiting state
            } catch (InterruptedException e) {
                System.out.println("Thread interrupted");
            }
        }
        System.out.println(Thread.currentThread().getName() + " received the update and is processing it.");
        updated = false; // Reset the state
    }

    public synchronized void updateResource() {
        System.out.println(Thread.currentThread().getName() + " is updating the resource...");
        updated = true;
        notify(); // Notify one waiting thread
    }

    public synchronized void notifyAllThreads() {
        System.out.println(Thread.currentThread().getName() + " is notifying all waiting threads...");
        updated = true;
        notifyAll(); // Notify all waiting threads
    }
}

class WaitingThread extends Thread {
    private final SharedResource resource;

    public WaitingThread(SharedResource resource) {
        this.resource = resource;
    }

    @Override
    public void run() {
        resource.waitForUpdate();
    }
}

class NotifyingThread extends Thread {
    private final SharedResource resource;

    public NotifyingThread(SharedResource resource) {
        this.resource = resource;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(3000); // Simulating a delay
        } catch (InterruptedException e) {
            System.out.println("Thread interrupted");
        }
        resource.updateResource(); // Update the resource and notify one thread
    }
}

public class NotifyExample {
    public static void main(String[] args) {
        SharedResource resource = new SharedResource();

        // Creating multiple threads waiting for the update
        WaitingThread t1 = new WaitingThread(resource);
        WaitingThread t2 = new WaitingThread(resource);
        WaitingThread t3 = new WaitingThread(resource);

        // Creating a notifying thread
        NotifyingThread t4 = new NotifyingThread(resource);

        t1.setName("Rohit");
        t2.setName("Sachin");
        t3.setName("Virat");
        t4.setName("Notifier");

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

5. Output (Using notify())

Rohit is waiting for an update...
Sachin is waiting for an update...
Virat is waiting for an update...
Notifier is updating the resource...
Rohit received the update and is processing it.

6. Output (Using notifyAll())

Rohit is waiting for an update...
Sachin is waiting for an update...
Virat is waiting for an update...
Notifier is notifying all waiting threads...
Rohit received the update and is processing it.
Sachin received the update and is processing it.
Virat received the update and is processing it.

7. Explanation

  1. Shared Resource:

    • Acts as the medium for communication between threads.

    • Maintains a state (updated) to indicate whether the resource has been modified.

  2. Thread Behavior:

    • Waiting Threads (t1, t2, t3):

      • Call wait() and enter the waiting state until notified.
    • Notifying Thread (t4):

      • Calls notify() to wake up one thread or notifyAll() to wake up all waiting threads.
  3. Lock Release:

    • The lock is released only after the synchronized block or method completes execution.
  4. Efficient Notification:

    • Prevents unnecessary waiting by allowing threads to proceed only when updates are available.

8. Diagram

Thread Communication Using notify()

Thread: Rohit (T1)              SharedResource                  Thread: Notifier (T4)
------------------------------------------------------------------------------------
Calls waitForUpdate()           Enters waiting state
                                --------------------------------
                                updated = false
                                --------------------------------
                                Releases lock
                                                                  Calls updateResource()
                                                                  --------------------------------
                                                                  updated = true
                                                                  Calls notify()
                                                                  --------------------------------
                                Wakes up one thread (T1)
                                --------------------------------
                                updated = false
                                --------------------------------
Continues execution

Thread Communication Using notifyAll()

Thread: T1, T2, T3              SharedResource                  Thread: Notifier (T4)
------------------------------------------------------------------------------------
Call waitForUpdate()            Enter waiting state
                                --------------------------------
                                updated = false
                                --------------------------------
                                Release lock
                                                                  Calls notifyAll()
                                                                  --------------------------------
                                                                  updated = true
                                                                  Wakes up all threads
                                                                  --------------------------------
T1, T2, T3 compete for lock
Each thread continues execution sequentially

9. Real-World Applications

  1. Event Notification:

    • Servers notifying clients when data becomes available.
  2. Thread Pools:

    • Worker threads waiting for tasks to be added to a queue.
  3. Resource Management:

    • Coordinating access to shared resources in multithreaded applications.

10. Key Observations

  1. Thread Synchronization:

    • Ensures efficient communication between threads sharing a resource.
  2. Correct Usage:

    • notify() is ideal for waking a single thread, while notifyAll() is better for systems where multiple threads can act on the same update.
  3. Avoiding Errors:

    • These methods must always be called within a synchronized block or method to prevent runtime exceptions.

11. Conclusion

The notify() and notifyAll() methods are critical for inter-thread communication in Java. By allowing threads to signal each other about resource updates, they ensure efficient resource utilization and prevent unnecessary waiting. When used correctly, they form the foundation for effective thread coordination in concurrent programming.

Important Points on Inter-Thread Communication in Java


1. Theory

Inter-thread communication is a powerful mechanism in Java to enable threads to communicate with each other. This communication allows threads to cooperate and coordinate actions while sharing resources. The methods used for inter-thread communication in Java come from different classes:

  • Thread Class: yield(), sleep(), and join() are methods that control the execution of threads.

  • Object Class: wait(), notify(), and notifyAll() are methods used to facilitate communication between threads.

Understanding the distinction and proper usage of these methods is essential for developing efficient and thread-safe applications.


2. Methods for Inter-Thread Communication

Thread Class Methods
  1. yield():

    • Causes the current thread to temporarily pause and allow other threads of the same priority to execute.

    • Does not release the lock; it simply signals the scheduler to give a chance to other threads.

  2. sleep(long ms):

    • Puts the current thread to sleep for the specified duration.

    • Does not release the lock; the thread will resume after the sleep period expires.

  3. join():

    • Makes the current thread wait until the thread on which join() is called finishes execution.

    • Does not release the lock during the waiting period; the current thread waits for the other thread to complete.

Object Class Methods
  1. wait():

    • Causes the current thread to wait until another thread sends a signal (via notify() or notifyAll()).

    • Releases the lock on the object while waiting.

  2. notify():

    • Wakes up a single thread that is waiting on the object's monitor.

    • Releases the lock once the notification is sent, allowing the waiting thread to acquire the lock.

  3. notifyAll():

    • Wakes up all threads that are waiting on the object's monitor.

    • Releases the lock once the notification is sent.


3. Key Points

  1. Thread Class vs Object Class:

    • yield(), sleep(), and join() are methods of the Thread class, which are used for thread control.

    • wait(), notify(), and notifyAll() are methods of the Object class, which allow threads to communicate and synchronize with each other.

  2. Releasing the Lock:

    • Thread Class Methods (yield(), sleep(), and join()) do not release the lock. These methods do not engage in inter-thread communication.

    • Object Class Methods (wait(), notify(), and notifyAll()) release the lock, allowing other threads to interact with the shared resource. This is essential for effective inter-thread communication.

  3. When a Thread Calls wait(), notify(), or notifyAll():

    • The calling thread releases the lock on the object it is waiting or notifying on, allowing other threads to acquire the lock and proceed with execution.

4. Why Are wait(), notify(), and notifyAll() in the Object Class?

Interview Question:
"Why are the methods wait(), notify(), and notifyAll() present in the Object class, not in the Thread class?"

Answer:

  • The wait(), notify(), and notifyAll() methods are used to synchronize and coordinate threads working on shared resources.

  • In Java, every object can serve as a monitor (lock) for thread synchronization. Therefore, the methods that are used for communication between threads must be available for all objects.

  • Since every object in Java inherits from the Object class, it makes sense for these communication methods to be defined in the Object class so that they can be used on any object.

  • If these methods were in the Thread class, they would only be available to thread objects, which would limit their functionality to only the threads themselves and would not allow inter-thread communication based on shared resources like postboxes, stacks, or customer-service queues.

By placing these methods in the Object class, every object in Java becomes a potential point of communication for threads, allowing synchronization and communication to occur on any shared resource.


5. Method Prototypes for wait(), notify(), and notifyAll()

  • wait():

      public final void wait() throws InterruptedException
    
    • Causes the current thread to wait until it is awakened by a call to notify() or notifyAll() on the same object.
  • wait(long ms):

      public final native void wait(long ms) throws InterruptedException
    
    • Causes the current thread to wait for the specified amount of time in milliseconds or until it is awakened by notify() or notifyAll().
  • wait(long ms, int ns):

      public final void wait(long ms, int ns) throws InterruptedException
    
    • Causes the current thread to wait for the specified time in milliseconds and nanoseconds.
  • notify():

      public final native void notify()
    
    • Wakes up one thread that is waiting on the object's monitor.
  • notifyAll():

      public final void notifyAll()
    
    • Wakes up all threads that are waiting on the object's monitor.

6. Example: Using wait() and notify()

Here’s an example that demonstrates the use of wait() and notify() for thread synchronization:

class Postbox {
    private boolean letterAvailable = false;

    public synchronized void waitForLetter() {
        while (!letterAvailable) {
            try {
                System.out.println(Thread.currentThread().getName() + " is waiting for the letter...");
                wait(); // The thread waits and releases the lock
            } catch (InterruptedException e) {
                System.out.println("Thread interrupted");
            }
        }
        System.out.println(Thread.currentThread().getName() + " received the letter!");
        letterAvailable = false;
    }

    public synchronized void deliverLetter() {
        System.out.println(Thread.currentThread().getName() + " delivered the letter.");
        letterAvailable = true;
        notify(); // Notify one waiting thread
    }
}

class Person1 extends Thread {
    private final Postbox postbox;

    public Person1(Postbox postbox) {
        this.postbox = postbox;
    }

    @Override
    public void run() {
        postbox.waitForLetter();
    }
}

class Postman extends Thread {
    private final Postbox postbox;

    public Postman(Postbox postbox) {
        this.postbox = postbox;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(3000); // Simulate a delay
        } catch (InterruptedException e) {
            System.out.println("Thread interrupted");
        }
        postbox.deliverLetter(); // Notify the waiting thread
    }
}

public class InterThreadCommunication {
    public static void main(String[] args) {
        Postbox postbox = new Postbox();
        Person1 person1 = new Person1(postbox); // Waiting thread
        Postman postman = new Postman(postbox); // Notifying thread

        person1.setName("Rohit");
        postman.setName("Postman");

        person1.start();
        postman.start();
    }
}

7. Output

Rohit is waiting for the letter...
Postman delivered the letter.
Rohit received the letter!

8. Diagram: Inter-Thread Communication

Here’s how the communication happens between the two threads using wait() and notify():

Thread: Person1 (T1)             Shared Resource (Postbox)              Thread: Postman (T2)
------------------------------------------------------------------------------------------
Calls waitForLetter()            Enters synchronized block
Enters waiting state             ---------------------------
                                Letter not available
                                ---------------------------
                                Releases lock
                                                                  Calls deliverLetter()
                                                                  ---------------------
                                                                  Letter available
                                                                  Notifies waiting thread (T1)
                                ---------------------------
                                Wakes up and acquires lock
                                ---------------------------
Resumes execution

9. Real-World Applications

  1. Producer-Consumer Problem:

    • Producers add items to a buffer, and consumers process them. wait() ensures consumers pause until producers add items, while notify() wakes consumers when an item is available.
  2. Task Scheduling:

    • Workers wait for tasks to be added to a queue. The task manager notifies workers when a task is available.
  3. Event-Driven Programming:

    • Threads waiting for events to occur, like GUI applications waiting for user input, and notified when the event occurs.

10. Key Observations

  1. Efficient Resource Utilization:

    • Using wait(), notify(), and notifyAll() helps avoid busy-waiting, allowing threads to release the lock when waiting for a condition to be met.
  2. Synchronization:

    • These methods must be used within synchronized blocks or methods to avoid IllegalMonitorStateException.
  3. Thread Coordination:

    • The proper use of these methods ensures that threads coordinate their actions without unnecessary CPU usage.

11. Conclusion

Inter-thread communication is an essential mechanism for efficient thread coordination.

Thread Life Cycle When a Thread Calls the wait() Method

In Java, the lifecycle of a thread can transition through various states such as New, Ready/Runnable, Running, Waiting, and Dead. The wait() method plays a crucial role in controlling thread execution, particularly in the Waiting state. Let's break down how the thread lifecycle operates when a thread calls the wait() method.

1. Thread Lifecycle Overview

  • New/Born: When a thread is created but not yet started, it is in the New state.

  • Ready/Runnable: When the thread is ready to run, it enters the Ready/Runnable state, waiting for the CPU to allocate time.

  • Running: Once the CPU assigns the thread time to execute, it moves to the Running state.

  • Waiting: If a thread calls the wait() method, it enters a Waiting state, pausing its execution until it is notified or a specified time elapses.

  • Dead: After execution finishes, the thread enters the Dead state.

Here’s a diagram illustrating the thread lifecycle:

                  +--------------------+
                  |                    |
          New/Born --> Ready/Runnable  ---> Running ---> Dead
                  |                    |
                  +----> Waiting State <--+
                          |          |
               When thread calls wait() |
                 (awaits lock release) |
                          |
            If notified or time expires
                          |
               Back to Ready/Runnable

2. Thread Entering the Waiting State

A thread enters the Waiting state when it calls the wait() method. The wait() method is usually called on a shared object (i.e., obj.wait()), and it causes the calling thread to pause its execution until it is either notified or a timeout occurs.

  • Example:

      synchronized (obj) {
          obj.wait(); // Thread enters waiting state
      }
    
  • Variants:

    • obj.wait(): The thread waits indefinitely until it is notified.

    • obj.wait(100): The thread waits for a specified time (100 milliseconds in this case) before automatically returning to the ready state if not notified.

    • obj.wait(100, 10): Waits for exactly 100 milliseconds and 10 nanoseconds.

3. Transition to Another Waiting State

After calling wait(), the thread enters another waiting state, where it waits to acquire the lock from the notifying thread. The thread doesn't actually proceed until it is notified by another thread.

  • Thread is in Waiting: The thread is now waiting for a notification to resume execution.

  • Notified or Interrupted: Once it is notified, or if its waiting time expires or it is interrupted, the thread will exit the Waiting state and attempt to move back to the Ready/Runnable state.

4. Possibilities of Coming Out of Waiting State

A thread can exit the Waiting state and re-enter the Ready/Runnable state through various possibilities:

  1. Notification:

    • When a thread calls notify() or notifyAll() on the same object that the waiting thread is waiting on, the waiting thread will be notified and can move out of the Waiting state.

    • Example:

        synchronized (obj) {
            obj.notify(); // Notifies one waiting thread
        }
      
  2. Timeout Expiration:

    • If the thread was waiting with a timeout (using wait(long timeout)), and the specified time expires, it will automatically exit the Waiting state and transition to Ready/Runnable.
  3. Thread Interruption:

    • If the thread is interrupted by another thread while it is waiting, it will come out of the Waiting state and be moved to the Ready/Runnable state. The InterruptedException is thrown in this case.

    • Example:

        synchronized (obj) {
            try {
                obj.wait();
            } catch (InterruptedException e) {
                // Handle interruption
            }
        }
      

5. Real-World Analogy

Imagine a restaurant scenario:

  • The thread is the waiter, and the lock is the kitchen's cooking space.

  • If the waiter (thread) has no orders (execution blocked), they wait in the kitchen (waiting state) for the chef (notification thread) to notify them when the order is ready (notification or timeout).

  • Once notified, the waiter (thread) can resume work and serve the customer (ready state).

Conclusion

In summary, when a thread calls the wait() method, it enters a Waiting state, and it will only resume when notified, when the specified timeout expires, or when it is interrupted. This mechanism is essential for managing synchronization between threads and ensuring proper execution flow in multithreading scenarios.

Inter-Thread Communication in Java

In a multithreading environment, it is important to manage how threads communicate and synchronize with each other. One of the core mechanisms that enable thread synchronization is the use of the wait() and notify() methods. These methods allow threads to share data safely and coordinate their execution. Below is a detailed explanation, example, and real-world analogy illustrating Inter-Thread Communication in Java.


1. Understanding Inter-Thread Communication

Inter-thread communication allows threads to communicate with one another and share information. In our case, we will focus on how the child thread performs a calculation, and how the main thread waits for the child thread to complete before accessing the result.

At any given point in time, data must be accessed by only one thread. For example, the data stored in a variable should only be modified by one thread at a time. The main thread should only access the data once the child thread has completed its task.

Core Concepts:

  • Synchronized Block: This ensures that only one thread can execute a block of code at a time.

  • wait(): Used by a thread to release the lock and enter the waiting state.

  • notify(): Used by a thread to wake up a waiting thread and give it the lock.


2. Steps of Inter-Thread Communication

The following steps are involved in the inter-thread communication between the main thread and the child thread:

Step 1: Main Thread Calls wait()

The main thread begins by calling the wait() method on an object that is shared between the threads. This causes the main thread to release the lock on that object and enter the waiting state. The main thread will remain in the waiting state until it is notified.

synchronized(b) {
    System.out.println("Main thread calling wait() method");
    b.wait();  // Main thread enters waiting state
    System.out.println("Main thread got notification call");
    System.out.println(b.total);  // Print the result once notified
}

Step 2: Child Thread Does the Calculation

The child thread begins execution and calculates the sum of the numbers (1 to 100 in this case). It performs the calculation within a synchronized block to ensure that no other thread can access the shared variable total while the calculation is in progress.

synchronized(this) {
    System.out.println("Child thread started the calculation");
    for (int i = 1; i <= 100; i++) {
        total += i;
    }
    System.out.println("Child thread giving notification call");
    this.notify();  // Child thread notifies main thread
}

Step 3: Child Thread Calls notify()

Once the child thread completes its task, it calls notify() on the same object (this), which signals the waiting main thread that it can now resume execution. The child thread releases the lock, allowing the main thread to acquire it.

Step 4: Main Thread Receives Notification

After the child thread calls notify(), the main thread will resume its execution, having been notified that the child thread has completed its task. The main thread then prints the result stored in the total variable.


3. Code Example: Inter-Thread Communication

Here’s the complete code demonstrating inter-thread communication in Java:

class ThreadB extends Thread {
    // Shared variable to store the result
    int total = 0;

    // Child thread performs the calculation
    @Override
    public void run() {
        synchronized (this) {  // Synchronize on the current object
            System.out.println("Child thread started the calculation");
            for (int i = 1; i <= 100; i++) {
                total += i;  // Summing up numbers from 1 to 100
            }
            System.out.println("Child thread giving notification call");
            this.notify();  // Notify the waiting thread (main thread)
        }
    }
}

public class Multithreading73 {
    public static void main(String[] args) throws InterruptedException {
        ThreadB b = new ThreadB();
        b.start();  // Start the child thread

        // Main thread waits for the child thread to finish
        synchronized (b) {  // Synchronize on the same object
            System.out.println("Main thread calling wait() method");
            b.wait();  // Main thread enters waiting state
            System.out.println("Main thread got notification call");
            System.out.println(b.total);  // Print the result from the child thread
        }
    }
}

4. Expected Output

When the code is executed, the output will look like this:

Main thread calling wait() method
Child thread started the calculation
Child thread giving notification call
Main thread got notification call
5050

5. Thread Scheduler and Thread Interaction

In the above example, there are two threads:

  1. The Main thread (priority 5)

  2. The Child thread (priority 5)

Although both threads have the same priority, the thread scheduler gives preference to the main thread after it calls wait(). When the child thread completes its task, it calls notify() on the shared object, signaling the main thread to resume.

Diagram:

                    +-------------------------+
                    |                         |
        Main Thread --> Waiting State --> Ready/Runnable ---> Running ---> Dead
                    |                         |
                    +------------+------------+
                                 |
                       Child Thread Starts Calculation
                                 |
                            +----+----+
                            |         |
                        Child Thread  --> Waiting for Lock --> Calculating Sum
  • Main thread: The main thread waits until the child thread completes its work.

  • Child thread: After completing the calculation, it notifies the main thread.


6. Real-World Analogy

Imagine a scenario where:

  • The Main thread is a waiter.

  • The Child thread is the chef.

  • The Shared object (total) is the order slip containing the total sum of dishes.

  • The waiter (main thread) is waiting for the chef (child thread) to finish preparing the dishes and fill in the total order amount.

  • The waiter (main thread) waits in the kitchen until the chef finishes cooking, and then the chef notifies the waiter that the order is complete (using notify()).

  • Once notified, the waiter (main thread) can proceed with the task and serve the customer (print the total).


Conclusion

In this example, inter-thread communication is achieved using the wait() and notify() methods. The main thread waits until it is notified by the child thread that it has completed the calculation. This ensures proper synchronization and safe sharing of data between threads.

Expanded Explanation: Inter-Thread Communication and Thread Synchronization in Java

In multithreading, Inter-Thread Communication refers to the process where threads communicate with each other to share data, resources, or trigger specific actions. This is particularly useful when threads must wait for each other to complete tasks or share the results of computations. Java provides mechanisms like synchronized blocks, wait(), notify(), and notifyAll() for effective communication and synchronization between threads.

Below, I will explain the different code examples from the previous code without synchronization section, starting from basic examples to more advanced usage. I will provide the theory, examples, diagrams, and real-world analogies.

1. Previous Code Without Synchronization (Basic Example)

In the first example, two threads (main and child) perform operations, but without synchronization:

class ThreadB extends Thread {
    int total = 0;

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            total += i;
        }
    }
}

public class Multithreading74 {
    public static void main(String[] args) {
        ThreadB b = new ThreadB();
        b.start();
        System.out.println(b.total);
    }
}

Explanation:

  • The ThreadB class runs a loop from 1 to 100, calculating the sum of integers and storing the result in the total variable.

  • Issue: When the main thread prints b.total, it may print 0 because the child thread may not have finished executing before the main thread accesses the total variable. Since no synchronization is applied, there is no guarantee of thread execution order.

Output:

0

Real-World Analogy:

Imagine a factory where one worker (child thread) is assembling parts, and another worker (main thread) is inspecting the final product. Without any coordination between workers, the inspector might start checking the product before the assembly is complete, resulting in incorrect inspection.

2. Example 2: Using Sleep for Main Thread

In the second example, we make the main thread sleep for 2 seconds to give the child thread time to execute:

class ThreadB extends Thread {
    int total = 0;

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            total += i;
        }
    }
}

public class Multithreading74 {
    public static void main(String[] args) throws InterruptedException {
        ThreadB b = new ThreadB();
        b.start();

        // Main thread sleeps for 2 seconds
        Thread.sleep(2000);
        System.out.println(b.total);
    }
}

Explanation:

  • In this example, the main thread sleeps for 2 seconds to ensure that the child thread has enough time to compute the sum.

  • Issue: While the main thread is sleeping, the child thread may or may not have completed the task. In this case, after 2 seconds, the child thread is likely finished, and the main thread prints the correct sum.

Output:

5050

Real-World Analogy:

This can be likened to one worker (main thread) waiting at a station for a report (sum) from another worker (child thread) after they have completed their task. However, relying on waiting time (sleep) without synchronization is not efficient, as it might not always guarantee the task completion.

3. Example 3: Using join() Method

In this example, the main thread waits for the child thread to complete using the join() method:

class ThreadB extends Thread {
    int total = 0;

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            total += i;
        }
    }
}

public class Multithreading74 {
    public static void main(String[] args) throws InterruptedException {
        ThreadB b = new ThreadB();
        b.start();

        // Main thread waits for child thread to complete
        b.join();
        System.out.println(b.total);
    }
}

Explanation:

  • The join() method ensures that the main thread waits for the child thread to finish execution before proceeding.

  • Issue: Although the main thread waits for the child thread to complete, using join() for a simple task can make the process inefficient. If the child thread contains a significant amount of computation (e.g., 100,000 lines of code), it could delay the main thread unnecessarily.

Output:

5050

Real-World Analogy:

Imagine a manager (main thread) waiting for a worker (child thread) to finish assembling a product before they can present it to the client. If the worker takes too long, the manager might waste time waiting even when it's not necessary.

4. Example 4: Using Active Count Method

The activeCount() method provides the number of active threads in the current thread group:

class ThreadB extends Thread {
    int total = 0;

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            total += i;
        }
    }
}

public class Multithreading74 {
    public static void main(String[] args) throws InterruptedException {
        ThreadB b = new ThreadB();
        b.start();

        // Display active thread count
        System.out.println("No of threads active: " + Thread.activeCount());
        b.join();
        System.out.println(b.total);
    }
}

Explanation:

  • The Thread.activeCount() method is used to determine how many threads are currently active in the thread group.

  • Usage: This method can be useful for monitoring the number of active threads, ensuring that thread management is performed effectively.

Output:

No of threads active: 2
5050

Real-World Analogy:

This method can be likened to a supervisor checking how many workers are actively engaged in a task. It helps track productivity and thread management.

5. Summary of Synchronization Challenges and Solutions

In multithreading, synchronization ensures that only one thread accesses a critical section of code at a time. Without synchronization, you risk issues like race conditions, where multiple threads access and modify shared data concurrently, leading to inconsistent or incorrect results.

Common Issues:

  • Race Conditions: Without synchronization, threads can alter shared data unpredictably.

  • Deadlocks: Threads waiting indefinitely for resources locked by each other.

  • Thread Interference: Threads interfering with each other's execution, resulting in errors or inconsistent output.

Solutions:

  • Synchronized Blocks/Methods: Using the synchronized keyword ensures only one thread can access a critical section at a time.

  • wait() and notify() Methods: These are used for inter-thread communication, allowing threads to wait for certain conditions or notify others when tasks are complete.

6. Real-World Example: Bank Transaction Simulation

Consider a bank account where multiple threads represent different users trying to withdraw money simultaneously. Without synchronization, two users may withdraw money at the same time, leading to an inconsistent balance. By using synchronization, we ensure that only one user can access and modify the balance at a time, maintaining data consistency.


Making Main Thread Sleep in Java Multithreading

In multithreading, the main thread is the initial thread of execution, while child threads are created by the main thread. Understanding how to manage these threads, especially when using methods like sleep(), wait(), and notify(), is crucial for effective synchronization and coordination between the threads.

Here, we explore different examples of how the sleep() and wait() methods work in Java, along with how they interact with child threads. The examples include both the issues that arise when these methods are not synchronized properly, and how synchronization can solve these problems.

Example 1: Main Thread Sleeping for 3 Seconds (Deadlock Scenario)

In this example, we see how the main thread sleeps for 3 seconds, which causes it to miss a notification from the child thread. This results in a deadlock scenario where the main thread enters an infinite wait.

Theory:

  • The Thread.sleep(3000) method makes the main thread sleep for 3 seconds.

  • During this time, the child thread executes its task, but the main thread misses the notification call because it is in a sleeping state.

  • The main thread will eventually wake up, but by this time, the child thread may have already finished its task and entered the dead state.

  • This causes the main thread to be stuck in an infinite wait, waiting for a notification that will never come, leading to a deadlock.

Code:

class ThreadB extends Thread {
    int total = 0;

    @Override
    public void run() {
        synchronized(this) {
            System.out.println("Child thread started the calculation");
            for (int i = 1; i <= 100; i++) {
                total += i;
            }
            System.out.println("Child thread giving notification call");
            this.notify();
        }
    }
}

public class Multithreading75 {
    public static void main(String[] args) throws InterruptedException {
        ThreadB b = new ThreadB();
        b.start();

        Thread.sleep(3000); // Main thread sleeps for 3 seconds

        synchronized(b) {
            System.out.println("Main thread calling wait() method");
            b.wait(); // Main thread waits indefinitely
            System.out.println("Main thread got notification call");
            System.out.println(b.total);
        }
    }
}

Output:

Child thread started the calculation
Child thread giving notification call
Main thread calling wait() method

In this scenario, the main thread is unable to receive the notification, causing it to wait indefinitely.

Example 2: Main Thread Waits for 1 Second

In this example, the main thread sleeps for 3 seconds, and then it calls the wait(1000) method, making it wait for 1 second. The child thread notifies the main thread after it finishes the task, which happens after 3 seconds.

Theory:

  • The wait(1000) method allows the main thread to wait for up to 1 second.

  • If the child thread notifies the main thread within this time, it will receive the notification and continue its execution.

  • If the child thread finishes the task after the main thread's 1-second wait, the main thread will still receive the notification, but with a slight delay.

Code:

class ThreadB extends Thread {
    int total = 0;

    @Override
    public void run() {
        synchronized(this) {
            System.out.println("Child thread started the calculation");
            for (int i = 1; i <= 100; i++) {
                total += i;
            }
            System.out.println("Child thread giving notification call");
            this.notify();
        }
    }
}

public class Multithreading75 {
    public static void main(String[] args) throws InterruptedException {
        ThreadB b = new ThreadB();
        b.start();

        Thread.sleep(3000); // Main thread sleeps for 3 seconds

        synchronized(b) {
            System.out.println("Main thread calling wait() method");
            b.wait(1000); // Main thread waits for 1 second
            System.out.println("Main thread got notification call");
            System.out.println(b.total);
        }
    }
}

Output:

Child thread started the calculation
Child thread giving notification call
Main thread calling wait() method
Main thread got notification call
5050

In this case, the main thread waits for 1 second, receives the notification from the child thread, and then prints the total.

Example 3: Main Thread Waits for 1 Second and Child Thread Sleeps for 3 Seconds

In this example, the main thread waits for 1 second, and the child thread sleeps for 3 seconds before notifying the main thread. This scenario demonstrates how synchronization and the timing of wait/notify can affect thread coordination.

Theory:

  • The main thread waits for a notification, but it only waits for 1 second before it gives up and proceeds.

  • The child thread sleeps for 3 seconds, which is longer than the main thread's wait time.

  • The main thread will eventually get the notification after waiting for the child thread to finish.

Code:

class ThreadB extends Thread {
    int total = 0;

    @Override
    public void run() {
        synchronized(this) {
            System.out.println("Child thread started the calculation");
            for (int i = 1; i <= 100; i++) {
                total += i;
            }

            try {
                Thread.sleep(3000); // Child thread sleeps for 3 seconds
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("Child thread giving notification call");
            this.notify();
        }
    }
}

public class Multithreading75 {
    public static void main(String[] args) throws InterruptedException {
        ThreadB b = new ThreadB();
        b.start();

        synchronized(b) {
            System.out.println("Main thread calling wait() method");
            b.wait(1000); // Main thread waits for 1 second
            System.out.println("Main thread got notification call");
            System.out.println(b.total);
        }
    }
}

Output:

Main thread calling wait() method
Child thread started the calculation
Child thread giving notification call
Main thread got notification call
5050

In this case, the main thread waits for 1 second, but the child thread does not notify until after 3 seconds. The main thread proceeds and prints the total after receiving the notification.

Conclusion

  • The Thread.sleep() method allows a thread to sleep for a specified period, releasing the CPU to other threads. However, it can cause synchronization issues if combined with the wait() method, as seen in the deadlock example.

  • The wait() method allows a thread to release the lock and wait for a notification. If combined with sleep() incorrectly, it may lead to deadlocks or missed notifications.

  • Synchronization (synchronized) is key when dealing with shared resources between threads. Proper management of thread states like wait(), sleep(), and notify() ensures smooth execution and avoids issues like deadlocks.

These examples demonstrate how thread synchronization works in practice and the challenges that arise when managing multiple threads in Java.

Producer and Consumer Problem in Java - Synchronization Example

The Producer-Consumer Problem is a classic synchronization problem where two threads, the Producer and the Consumer, operate on a shared resource, which is typically a queue. The Producer produces items and puts them in the queue, while the Consumer takes items from the queue to consume them. The challenge is to synchronize these threads in a way that ensures the Producer does not add items to the queue when it’s full, and the Consumer does not attempt to consume from the queue when it’s empty.

Conceptual Overview:

  1. Producer: The thread responsible for producing an item and adding it to a shared queue. After adding the item, it notifies the Consumer that an item is available.

  2. Consumer: The thread responsible for consuming an item from the queue. If the queue is empty, the Consumer must wait until the Producer adds an item.

In this problem, the wait() and notify() methods are used to synchronize the threads.

Visual Representation:

                          _________
                         |         |
   Producer ------------> |  Queue  | <---------- Consumer
    (produce)            |_________|              (consume)
         |                                        |
         |               notify()                | wait()
         |----------------------------------------|

Working:

  • Producer: Adds items to the queue and then notifies the Consumer that an item is available.

  • Consumer: Waits for an item to be available in the queue, consumes it when available, and then continues or waits again if the queue is empty.

Code Explanation:

class Queue {
    int item;
    boolean available = false;

    // Producer produces an item and puts it in the queue
    public synchronized void produce(int item) throws InterruptedException {
        while (available) {
            wait(); // Wait if the item is already available
        }
        this.item = item;
        System.out.println("Produced item: " + item);
        available = true;
        notify(); // Notify Consumer that an item is available
    }

    // Consumer consumes the item from the queue
    public synchronized int consume() throws InterruptedException {
        while (!available) {
            wait(); // Wait if the item is not available
        }
        System.out.println("Consumed item: " + item);
        available = false;
        notify(); // Notify Producer that the item was consumed
        return item;
    }
}

class Producer extends Thread {
    Queue queue;

    public Producer(Queue queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            int item = 0;
            while (true) {
                queue.produce(item++);
                Thread.sleep(1000); // Simulate time taken to produce an item
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Consumer extends Thread {
    Queue queue;

    public Consumer(Queue queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            while (true) {
                queue.consume();
                Thread.sleep(2000); // Simulate time taken to consume an item
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Queue queue = new Queue();
        Producer producer = new Producer(queue);
        Consumer consumer = new Consumer(queue);

        producer.start();
        consumer.start();
    }
}

Explanation:

  1. Queue class: This is the shared resource that both the Producer and the Consumer use. It has a produce method to add items and a consume method to remove them.

    • synchronized ensures that only one thread can access the produce or consume method at a time.

    • wait() is used to make a thread wait when the queue is in a state where it cannot proceed (e.g., when the queue is full or empty).

    • notify() wakes up a waiting thread when the state of the queue changes (e.g., when an item is produced or consumed).

  2. Producer thread: The producer continuously produces items and adds them to the queue.

  3. Consumer thread: The consumer consumes items from the queue whenever an item is available.

Difference Between notify() and notifyAll()

  1. notify(): Notifies a single thread waiting on the object. The exact thread that gets notified is determined by the thread scheduler.

  2. notifyAll(): Notifies all threads waiting on the object, and they will proceed to get a chance to execute one by one as they acquire the lock.

In cases where multiple threads are waiting for a resource, notify() can cause only one thread to wake up, while notifyAll() wakes up all waiting threads. The choice between them depends on the scenario and whether you want to give one thread or all threads a chance to proceed.

Example of notify() and notifyAll():

  • If there are multiple threads waiting on an object (e.g., a queue), calling notify() will wake up just one waiting thread, while notifyAll() will wake up all waiting threads.

  • notify() might be used when only one thread should proceed at a time, and notifyAll() is useful when all waiting threads should be given a chance to proceed, such as in a situation where multiple consumers or producers are involved.

IllegalMonitorStateException:

This exception occurs when a thread tries to call wait(), notify(), or notifyAll() without holding the lock on the object on which the method is being called. For instance, calling wait() on an object outside of a synchronized block will throw this exception because the thread is not holding the lock on the object.

Example 1 (Throws IllegalMonitorStateException):

Stack s1 = new Stack();
Stack s2 = new Stack();

s2.wait(); // This will throw IllegalMonitorStateException because s2 is not synchronized.

Example 2 (Valid Usage):

Stack s1 = new Stack();
Stack s2 = new Stack();

synchronized (s2) {
    s2.wait(); // This is valid because we have synchronized on s2.
}

Summary of Key Points:

  1. Producer-Consumer Problem: Producers and consumers share a common queue and must synchronize their operations using wait() and notify().

  2. notify() vs notifyAll(): Use notify() to wake up one waiting thread, and use notifyAll() to wake up all waiting threads.

  3. IllegalMonitorStateException: This exception occurs when wait(), notify(), or notifyAll() is called outside a synchronized block.

  4. Synchronization: Threads must acquire the lock of an object before calling wait(), notify(), or notifyAll() on it.


Conclusion:

In this chapter, we have explored the essential concepts of Synchronized Blocks and Inter-Thread Communication in Java. By understanding how synchronization works, we can prevent common concurrency issues, such as race conditions and data inconsistencies, when multiple threads are working with shared resources.

We began by examining the functionality of synchronized blocks, which allow you to control thread access to critical sections of code, ensuring thread safety. We also discussed how the synchronized keyword can be applied at both the block and method levels, and how class-level locks come into play when multiple threads interact with different objects.

The chapter also highlighted the importance of inter-thread communication, focusing on the methods wait(), notify(), and notifyAll(). These methods allow threads to pause their execution until a certain condition is met, thereby enabling threads to work together efficiently. By understanding when and how to use these methods, we can create responsive, multi-threaded applications.

Finally, we covered important exceptions, such as IllegalMonitorStateException, and clarified the life cycle of a thread when invoking wait(). This knowledge is critical for developing robust and error-free multi-threaded applications in Java.

In summary, mastering synchronized blocks and inter-thread communication is vital for any Java developer working with multi-threaded environments. With the insights and examples provided in this chapter, you now have the tools needed to build thread-safe, efficient, and scalable applications that can leverage the power of concurrency in Java.


Other Series:


Connect with Me
Stay updated with my latest posts and projects by following me on social media:

  • LinkedIn: Connect with me for professional updates and insights.

  • GitHub: Explore my repository and contributions to various projects.

  • LeetCode: Check out my coding practice and challenges.

Your feedback and engagement are invaluable. Feel free to reach out with questions, comments, or suggestions. Happy coding!


Rohit Gawande
Full Stack Java Developer | Blogger | Coding Enthusiast