Back

Inter-thread Communication in Java: Methods

18 Mar 2025
4 min read

inter thread communication in Java refers to the concepts that help synchronize and communicate efficiently between threads that are executing concurrently. It is a mechanism in which a thread is paused while running in its critical section and an other thread is allowed to enter (or lock) in the same critical section to be executed.It ensures the communication between the different threads in the same process acts smoothly to coordinate work, share resources, and perform related tasks. 

For a Java multi-threading application, it plays an essential role in coordinating resource management, minimising delays in shared resources, and preventing deadlock and race conditions. This article will explore inter thread communication in Java with its methods and examples.

What Is Inter-Process Communication (IPC) in Java?

Inter-Process Communication (IPC) is the kind of mechanism through which different processes communicate between themselves. In comparison to inter thread communication, which is the kind of communication including threads within a single process.

IPC in Java enables processes to share data and synchronize their actions, whereas, in the context of ITC, attention is directed towards threads within a single Java application. 

Java supports low-level synchronisation using wait()/notify() and higher-level constructs such as CyclicBarrier and Semaphore to implement efficient thread coordination and communication.

Java multithreading

Java multithreading is an implementation that allows simultaneous execution of two or more threads in a way that CPU resources are maximally utilized. It assists in running many tasks at a time within a program to optimize performance in programs like real-time systems, games, or complex simulations.

Understanding the Communication Between Threads Works

Inter-thread communication synchronizes threads, ensuring proper data sharing or control over execution flow. While one thread waits for a condition to be fulfilled, it calls wait(), releasing this lock. The other thread can then perform the task and notify the waiting thread using notify() or notifyAll(). The communication ensures that the threads work together and share the resources without race conditions or unnecessary instruction processor utilization.

Methods of Inter-Thread Communication in Java

custom img

There are a variety of methods of implementing inter thread communication in Java, such as:

1. wait() Method: This method is used when a thread releases the lock it held on a shared resource and goes into a waiting state. 

Wait () can be called by a thread only when it finds itself holding the monitor lock of the object on which it is synchronized. It waits for another thread to call, notify or notify all to resume its execution.

  • wait(long timeout): It will make the thread wait for the specified number of milliseconds before it resumes, even if no notification occurs.

2. notify() Method: This is when the thread has finished its job and wants to wake up another sleeping thread waiting on the same shared resource. The thread calls the notify() method on the object's monitor to inform one of the threads waiting on it and allows that thread to continue execution.

3. notifyAll() Method: The notifyAll() method ensures all the threads waiting on this object's monitor. Thereafter, the threads will compete for the lock.

4. Sleep (): The method sleep() is utilized to stop the execution of the current thread for some amount of time. It does not release any lock the thread has acquired or wait for other threads to signal it. 

Difference between wait and sleep?

Here are the key differences for wait and sleep:

wait() sleep()
This belongs to the Object class. This belongs to the Thread class.
It can only be called inside a synchronized block or method. It can be called at any time, whether in a synchronized block or not.
This releases the lock on the object so other threads can access the shared resource. Does not allow for releasing the lock on the object the thread had locked on the whole sleep duration.
The thread will wait until it receives a call notifying it to resume execution. The thread will halt for the stated time and continue automatically to resume with the time.

Example 1: Example where a thread uses data delivered by another thread without using wait() and notify() method.

// Shared Queue (Q) class to hold the data
class Q {
    int i;             // Data to be shared
    boolean valueSet = false; // Flag to track if data is set

    // Method for the producer to produce data
    synchronized void produce(int i) {
        if (valueSet) {
            try {
                wait();  // Wait if data is already produced
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }
        this.i = i;  // Set the data
        valueSet = true;
        System.out.println("Data Produced: " + i);
        notify();  // Notify the consumer that data is ready
    }

    // Method for the consumer to consume data
    synchronized int consume() {
        if (!valueSet) {
            try {
                wait();  // Wait if no data is produced
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }
        System.out.println("Data Consumed: " + i);  // Consume the data
        valueSet = false;
        notify();  // Notify the producer to produce more data
        return i;
    }
}

// Producer class to produce data
class Producer extends Thread {
    Q q;
    Producer(Q q) {
        this.q = q;
    }

    public void run() {
        for (int j = 1; j <= 5; j++) {
            q.produce(j);  // Produce 5 pieces of data
        }
    }
}

// Consumer class to consume data
class Consumer extends Thread {
    Q q;
    Consumer(Q q) {
        this.q = q;
    }

    public void run() {
        for (int k = 0; k < 5; k++) {
            q.consume();  // Consume 5 pieces of data
        }
    }
}

// Main class to start the threads
public class ThreadCommunication {
    public static void main(String[] args) {
        Q q = new Q(); // Create shared queue

        // Create producer and consumer threads
        Producer p = new Producer(q);
        Consumer c = new Consumer(q);

        // Start the threads
        p.start();
        c.start();
    }
}

Explanation

  • If data is already available, the producer thread waits. Otherwise, it creates data, sets the flag (valueSet = true), and then signals the consumer.
  • If data is not produced, the consumer thread will be blocked. If data is produced, it will consume the data, reset the flag (valueSet = false), and wake the producer to produce more data.
  • The producer makes data, and the consumer consumes them one by one. Both threads execute synchronously through wait() and notify().

Output

Data Produced: 1
Data Consumed: 1
Data Produced: 2
Data Consumed: 2
Data Produced: 3
Data Consumed: 3
Data Produced: 4
Data Consumed: 4
Data Produced: 5
Data Consumed: 5

Example 2 - rewriting the above program using wait() and notify() methods to establish the communication between two threads.

// Shared Buffer class to hold the data
class Buffer {
    int item;             // Data to be shared
    boolean isProduced = false; // Flag to track if data is produced

    // Method for the producer to produce data
    synchronized void produceItem(int item) {
        if (isProduced) {
            try {
                wait();  // Wait if data is already produced
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }
        this.item = item;  // Set the data
        isProduced = true;  // Mark data as produced
        System.out.println("Produced: " + item);
        notify();  // Notify the consumer that data is ready
    }

    // Method for the consumer to consume data
    synchronized int consumeItem() {
        if (!isProduced) {
            try {
                wait();  // Wait if no data is produced
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }
        System.out.println("Consumed: " + item);  // Consume the data
        isProduced = false;  // Mark data as consumed
        notify();  // Notify the producer to produce more data
        return item;
    }
}

// Producer class to produce data
class DataProducer extends Thread {
    Buffer buffer;
    
    DataProducer(Buffer buffer) {
        this.buffer = buffer;
    }

    public void run() {
        for (int i = 1; i <= 5; i++) {
            buffer.produceItem(i);  // Produce 5 items
        }
    }
}

// Consumer class to consume data
class DataConsumer extends Thread {
    Buffer buffer;
    
    DataConsumer(Buffer buffer) {
        this.buffer = buffer;
    }

    public void run() {
        for (int i = 0; i < 5; i++) {
            buffer.consumeItem();  // Consume 5 items
        }
    }
}

// Main class to start the threads
public class ThreadCommunicationDemo {
    public static void main(String[] args) {
        Buffer sharedBuffer = new Buffer(); // Create shared buffer

        // Create producer and consumer threads
        DataProducer producer = new DataProducer(sharedBuffer);
        DataConsumer consumer = new DataConsumer(sharedBuffer);

        // Start the threads
        producer.start();
        consumer.start();
    }
}

Explanation

  • Buffer class controls the shared resource, with produceItem() and consumeItem() methods to enable synchronization of Producer and Consumer through wait() and notify().
  • DataProducer class produces data items and invokes produceItem() to store the data into the shared buffer.
  • DataConsumer class reads data items and calls consumeItem() to retrieve the data from the shared buffer.
  • Main class initializes instances of Producer and Consumer, activates their threads, and allows them to communicate messages through the shared Buffer.

Output

Produced: 1
Consumed: 1
Produced: 2
Consumed: 2
Produced: 3
Consumed: 3
Produced: 4
Consumed: 4
Produced: 5
Consumed: 5

Example 3: Producer-Consumer Problem Using Methods

Below is an example program that demonstrates the inter-thread communication between a producer and a consumer using the methods wait(), notify(), and notifyAll():

Java Code

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

public class ProducerConsumer {

    // 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) {
                    // Waits 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); // Simulate some delay in production
                    } 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); // Simulate some delay in consumption
                    } 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

  • Buffer class contains an integer item and a flag generated to indicate if the item is generated.
  • The producer() method places a product in the buffer and signals the consumer when complete.
  • The consumer() function consumes the product and informs the producer upon completion.
  • The Producer and Consumer classes consume and produce the items in a loop.
  • The main() function establishes the producer and consumer threads, initiates execution, and they communicate through the shared Buffer object.

Output

Produced: 1
Consumed: 1
Produced: 2
Consumed: 2
Produced: 3
Consumed: 3
Produced: 4
Consumed: 4
Produced: 5
Consumed: 5

Collaboration Using the java.util.concurrent Package

This java.util.concurrent package in Java provides higher-level constructs for simpler thread synchronization and communication. Some of these constructs include CyclicBarrier, Semaphore, CountDownLatch, and Exchanger. They allow developers more flexibility and power over inter-thread communication.

For example, one type of participant in the game might be a CyclicBarrier which allows several threads to wait for each other at a common barrier point so that the entire group of threads finishes before a single thread proceeds. 

What Is Polling, and What Are the Problems with It?

Polling is continuously checking a variable or condition until it is true. This means typically running a loop to check a condition continually. If the condition is true, some action will be taken. Despite being simple to work with, polling can be inefficient, wasting CPU resources by repeatedly checking for the condition without effectively doing productive work.

For example, in a producer-consumer problem in which data is produced by one thread and consumed by another, polling continuously checks if there is anything to consume from the queue or whether there is space to store more items; this cycle wastes CPU cycles while doing it.

Problems with Polling:

  • Waste of CPU resources: When a condition is being checked rigorously, the processor may be held busy, which could have been used for some productive task.
  • Wastage of CPU cycles: If the condition were absent, the thread would repeatedly check and waste useless CPU cycles.
  • Poor scalability: When many threads are involved, the polling mechanism can significantly deteriorate performance due to the many threads checking the condition over.

How Java Multi-Threading Tackles This Problem?

Java also avoids polling through the provision of inter-thread communication methods such as wait(), notify(), and notifyAll(), which allow specific threads to wait for certain conditions to be satisfied without polling, leading to better performance and more efficient utilization of resources.

Conclusion

In conclusion, Inter-thread communication in Java is essential for efficient thread synchronisation. The mechanisms of wait(), notify(), or notifyAll() let the developers coordinate threads to avoid race conditions and deadlocks. The Java Concurrency package can further simplify the procedures for the developer and aid them in providing high-level synchronisation. Developers can utilize the issue of inter-thread communication to build efficient, maintainable, responsive Java applications.

Frequently Asked Questions

1. What is Inter-Process Communication in multithreading?

The automatic and program-internal communication of separate processes or threads is what Inter-Process Communication in multithreading implies. It guarantees smooth coordination and proper handling of resources, guaranteeing no conflicts from multiple threads in a program and no deadlocks.

2. What is two-way communication in Java?

Two-way communication in Java allows either of the two communicating threads/processes to send data back and forth to each other. This may be accomplished using wait(), notify(), or higher-level constructs such as BlockingQueue.

3. Why is Java called a multithreaded Language?

Java is called a multithreaded language because it enables concurrent execution of multiple threads that share a single process. Java's built-in synchronization and communication facility ensures that no thread executes without interference from others.

4. How many threads will Java allow?

Java runs on one or more threads. The main thread created is one of several threads in a program. Depending on the task for which it is being used, a Java-based program can have a different number of threads, created either using the Thread class or through thread pooling for concurrent execution.

Read More Articles

Chat with us
Chat with us
Talk to career expert