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.
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 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.
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.
There are a variety of methods of implementing inter-thread communication in Java:
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.
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.
The notifyAll() method ensures all the threads waiting on this object's monitor. Thereafter, the threads will compete for the lock.
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.
| 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 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();
}
}
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
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();
}
}
Produced: 1
Consumed: 1
Produced: 2
Consumed: 2
Produced: 3
Consumed: 3
Produced: 4
Consumed: 4
Produced: 5
Consumed: 5
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");
}
}
Produced: 1
Consumed: 1
Produced: 2
Consumed: 2
Produced: 3
Consumed: 3
Produced: 4
Consumed: 4
Produced: 5
Consumed: 5
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.
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.
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.
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.
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.
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.
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.
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