Inter-thread Communication in Java: Methods

Overview

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

Table of Contents

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

There are a variety of methods of implementing inter-thread communication in Java:

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 notifyAll to resume its execution.

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() Method

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()

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.

Code Examples

Example 1: Producer-Consumer Without wait() and notify()

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

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 with wait() and notify() Methods

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

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():

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

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

The 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

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

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

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.

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.

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.

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.


Published: 18 Mar 2025
Reading Time: 4 min read
Source: NxtWave - CCBP Blog