Open In App

Inter-thread Communication in Java

Last Updated : 18 Apr, 2025
Comments
Improve
Suggest changes
Like Article
Like
Report

Inter-thread communication in Java is a mechanism in which a thread is paused from running in its critical section, and another thread is allowed to enter (or lock) the same critical section to be executed.

Note: Inter-thread communication is also known as Cooperation in Java.

What is Polling, and What are the Problems with it? 

The process of testing a condition repeatedly till it becomes true is known as polling. Polling is usually implemented with the help of loops to check whether a particular condition is true or not. If it is true, a certain action is taken. For example, in a classic queuing problem where one thread is producing data, and the other is consuming it.

Problem with Polling:

  • This wastes many CPU cycles and makes the implementation inefficient. 
  • This slows down the execution, and it keeps on checking the condition repeatedly.

How does Java Multi-Threading Tackle this problem? 

To avoid polling, Java uses three methods, namely, wait(), notify(), and notifyAll(). All these methods belong to the object class, so all classes have them. They must be used within a synchronized block only. 

  • wait(): It tells the calling thread to give up the lock and go to sleep until some other thread enters the same monitor and calls notify().
  • notify(): It wakes up one single thread called wait() on the same object. It should be noted that calling notify() does not give up a lock on a resource.
  • notifyAll(): It wakes up all the threads called wait() on the same object.

The image below demonstrates the concept of Thread Synchronization and Inter-Thread Communication in Java:

Java Multithreading Tackles Polling


Producer-Consumer Problem

Now, we are going to understand what producer-consumer problem is. In this one thread add items to a queue and the another thread removes items from the queue. We are going to use wait(), notify() and nortifyAll() methods to ensure that both threads operate efficiently

Example: A simple Java program to demonstrate the three methods. Please note that this program might only run in offline IDEs as it contains taking input at several points.

Java
import java.util.LinkedList;
import java.util.Queue;

public class Geeks {
    
    // Shared queue used by both producer and consumer
    private static final Queue<Integer> queue = new LinkedList<>();
    
    // Maximum capacity of the queue
    private static final int CAPACITY = 10;

    // Producer task
    private static final Runnable producer = new Runnable() {
        public void run() {
            while (true) {
                synchronized (queue) {
                    
                    // Wait if the queue is full
                    while (queue.size() == CAPACITY) {
                        try {
                            System.out.println("Queue is at max capacity");
                            queue.wait(); // Release the lock and wait
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // Add item to the queue
                    queue.add(10);
                    System.out.println("Added 10 to the queue");
                    queue.notifyAll(); // Notify all waiting consumers
                    try {
                        Thread.sleep(2000); 
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    };

    // Consumer task
    private static final Runnable consumer = new Runnable() {
        public void run() {
            while (true) {
                synchronized (queue) {
                    
                    // Wait if the queue is empty
                    while (queue.isEmpty()) {
                        try {
                            System.out.println("Queue is empty, waiting");
                            queue.wait(); // Release the lock and wait
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // Remove item from the queue
                    System.out.println("Removed " + queue.remove() + " from the queue");
                    queue.notifyAll(); // Notify all waiting producers
                    try {
                        Thread.sleep(2000); 
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    };

    public static void main(String[] args) {
        System.out.println("Main thread started");
        
        // Create and start the producer thread
        Thread producerThread = new Thread(producer, "Producer");
        
        // Create and start the consumer thread
        Thread consumerThread = new Thread(consumer, "Consumer");
        producerThread.start();
        consumerThread.start();
        System.out.println("Main thread exiting");
    }
}

Explanation of the above Program:

Queue Initialization:

private static final Queue<Integer> queue = new LinkedList<>();

private static final int CAPACITY = 10;

A shared queue (LinkedList implementation) is defined with a capacity of 10. This queue will be used by both producer and consumer threads to add and remove items.

Producer Runnable:

private static final Runnable producer = new Runnable() {

public void run() {

while (true) {

synchronized (queue) {

while (queue.size() == CAPACITY) {

try {

System.out.println("Queue is at max capacity");

queue.wait(); // Release the lock and wait

} catch (InterruptedException e) {

e.printStackTrace();

}

}

queue.add(10);

System.out.println("Added 10 to the queue");

queue.notifyAll(); // Notify all waiting consumers

try {

Thread.sleep(2000); // Simulate some delay in production

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

};

  • Infinite Loop: The producer runs in an infinite loop, continuously producing items.
  • Synchronization: The block is synchronized on the shared queue to ensure thread safety.
  • Capacity Check: If the queue is full (queue.size() == CAPACITY), the producer waits (queue.wait()) until notified by a consumer.
  • Add Item: If there is space in the queue, the producer adds an item (queue.add(10)).
  • Notify Consumers: After adding an item, it notifies all waiting consumers (queue.notifyAll()).
  • Simulate Delay: The producer sleeps for 2 seconds to simulate the production time (Thread.sleep(2000)).

Consumer Runnable:

private static final Runnable consumer = new Runnable() {

public void run() {

while (true) {

synchronized (queue) {

while (queue.isEmpty()) {

try {

System.out.println("Queue is empty, waiting");

queue.wait(); // Release the lock and wait

} catch (InterruptedException e) {

e.printStackTrace();

}

}

System.out.println("Removed " + queue.remove() + " from the queue");

queue.notifyAll(); // Notify all waiting producers

try {

Thread.sleep(2000); // Simulate some delay in consumption

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

};

  • Infinite Loop: The consumer runs in an infinite loop, continuously consuming items.
  • Synchronization: The block is synchronized on the shared queue to ensure thread safety.
  • Empty Check: If the queue is empty (queue.isEmpty()), the consumer waits (queue.wait()) until notified by a producer.
  • Remove Item: If there are items in the queue, the consumer removes an item (queue.remove()).
  • Notify Producers: After removing an item, it notifies all waiting producers (queue.notifyAll()).
  • Simulate Delay: The consumer sleeps for 2 seconds to simulate the consumption time (Thread.sleep(2000)).

Main Method:

public static void main(String[] args) {

System.out.println("Main thread started");

// Create and start the producer thread

Thread producerThread = new Thread(producer, "Producer");

// Create and start the consumer thread

Thread consumerThread = new Thread(consumer, "Consumer");

producerThread.start();

consumerThread.start();

System.out.println("Main thread exiting");

}

  • Main Thread Start: The main method starts by printing a message.
  • Create Threads: Two threads are created one for the producer and the another one is for consumer

Thread producerThread = new Thread(producer, "Producer")

Thread consumerThread = new Thread(consumer, "Consumer")

  • Start Threads: Now, both threads are start

producerThread.start()

consumerThread.start()

  • Main Thread Exit: The main method prints an exit message and terminates.

Output:

The program will produce output similar to this:

Main thread started
Main thread exiting
Queue is empty, waiting
Added 10 to the queue
Removed 10 from the queue
Queue is empty, waiting
Added 10 to the queue
Removed 10 from the queue
...

Here, the output demonstrates the interaction between the producer and consumer. The producer adds items to the queue, and the consumer removes them, if the queue is full the producer waits and if the queue is empty the consumer waits. This show how two thread commuincate with each other without wasting time using wait() and notifyAll().


Next Article
Article Tags :
Practice Tags :

Similar Reads