CodeToConcept
About

java

java Tutorials

Introduction
Java Variables
Java Data Types
Java Type Casting
Java Operators
Java Input/Output
Java Class & Object
java Method
Java Constructor
Java this keyword
Java super keyword
Java Method Overloading and Overriding
Java Interface
Java Abstract Class
Java Memory Management
Design Patterns
JDBC (Database Connectivity)
Java IDEs

java Tutorials

Introduction
Java Variables
Java Data Types
Java Type Casting
Java Operators
Java Input/Output
Java Class & Object
java Method
Java Constructor
Java this keyword
Java super keyword
Java Method Overloading and Overriding
Java Interface
Java Abstract Class
Java Memory Management
Design Patterns
JDBC (Database Connectivity)
Java IDEs

Sample Question

Think it through, then check the tutorial.

CodeToConcept

Quickly learn through simple, structured, and practical coding tutorials. Build your confidence from step zero to real-world experience.

© 2026 CodeToConcept. All rights reserved.

Quick Links

AboutTutorials

Tutorials

JavaHTMLCSS

Support

Privacy PolicyTerms of ServiceContact
GitHubTwitterYouTube

Java Inter-Thread Communication

A thread sometimes needs to wait for a condition instead of repeatedly checking it. With wait(), notify(), and notifyAll(), threads coordinate using the same object monitor and condition. Waking up means the waiter must check again after it gets the lock.

These methods are defined in the Object class.

What is Inter-Thread Communication?

Inter-thread communication is a mechanism that allows one thread to wait for a condition or event and another thread to notify it when that condition may have changed.

For example:

Producer Thread
      ↓
Produces data
      ↓
Notifies
      ↓
Consumer Thread
      ↓
Processes data

Instead of repeatedly checking whether data is available, the consumer can wait until the producer notifies it.

Why is Inter-Thread Communication Needed?

Suppose one thread produces data and another thread consumes it.

Without proper coordination, the consumer might continuously check:

while (!dataAvailable) {
    // keep checking
}

This approach is called busy waiting or busy spinning.

It can unnecessarily consume CPU resources.

With inter-thread communication, the consumer can wait:

wait();

The producer can then notify the waiting thread:

notify();

This allows the waiting thread to pause until there is a reason to continue.

Methods Used for Inter-Thread Communication

Java's traditional monitor-based communication uses three methods:

MethodPurpose
wait()Makes the current thread wait and releases the object's monitor
notify()Wakes one waiting thread
notifyAll()Wakes all threads waiting on the same object's monitor

All three methods are defined in the Object class.

wait() Method

The wait() method causes the current thread to wait until another thread notifies it or until it is interrupted.

Example

synchronized (lock) {
    lock.wait();
}

When wait() is called:

  • The current thread must own the object's monitor.
  • The thread releases that object's monitor.
  • The thread enters the WAITING state.
  • It remains waiting until it is notified, interrupted, or otherwise becomes eligible to continue according to the synchronization protocol.
  • After being notified, it must reacquire the monitor before continuing.

Why Does wait() Release the Lock?

Suppose a thread calls:

synchronized (lock) {
    lock.wait();
}

If the thread kept holding the monitor while waiting, another thread would not be able to enter:

synchronized (lock) {
    // notify waiting thread
}

Therefore, wait() releases the monitor associated with that object while the thread waits.

Conceptually:

Thread-1
   ↓
owns lock
   ↓
wait()
   ↓
releases lock
   ↓
WAITING

Another thread can now acquire the same monitor.

notify() Method

The notify() method wakes one thread that is waiting on the same object's monitor.

Example

synchronized (lock) {
    lock.notify();
}

If multiple threads are waiting, notify() selects one waiting thread to wake.

The exact choice of which waiting thread is notified is not something application code should depend on.

notifyAll() Method

The notifyAll() method wakes all threads waiting on the same object's monitor.

Example

synchronized (lock) {
    lock.notifyAll();
}

All waiting threads become eligible to compete for the monitor.

However, they do not all execute the synchronized block simultaneously.

Only one thread can own the monitor at a time.

notify() vs notifyAll()

notify()notifyAll()
Wakes one waiting threadWakes all waiting threads
Useful when one waiting thread is sufficientUseful when multiple waiting threads may need to recheck a condition
Other waiting threads remain waitingOther waiting threads also become eligible to compete for the monitor

Example

lock.notify();

wakes one waiting thread. Whereas:

lock.notifyAll();

wakes all threads waiting on that monitor.

Important Rule: wait(), notify(), and notifyAll() Require the Monitor

These methods must be called by a thread that owns the monitor of the object.

Correct:

synchronized (lock) {
    lock.wait();
}

Correct:

synchronized (lock) {
    lock.notify();
}

Correct:

synchronized (lock) {
    lock.notifyAll();
}

Incorrect:

lock.wait();

without owning lock's monitor. This results in:

IllegalMonitorStateException

Basic Producer-Consumer Example

A common example of inter-thread communication is the producer-consumer problem.

The producer produces data. The consumer consumes data.

Producer
   ↓
Produces data
   ↓
Shared resource
   ↓
Consumer
   ↓
Consumes data

Let's create a simple example.

class SharedData {
 
    private int value;
    private boolean available = false;
 
    public synchronized void produce(int value) {
 
        while (available) {
            try {
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }
        }
 
        this.value = value;
        available = true;
 
        System.out.println("Produced: " + value);
 
        notifyAll();
    }
 
    public synchronized int consume() {
 
        while (!available) {
            try {
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return -1;
            }
        }
 
        int value = this.value;
        available = false;
 
        System.out.println("Consumed: " + value);
 
        notifyAll();
 
        return value;
    }
}

Here:

  • produce() waits if data is already available.
  • consume() waits if data is not available.
  • notifyAll() informs waiting threads that the shared state has changed.

Complete Producer-Consumer Example

class SharedData {
 
    private int value;
    private boolean available = false;
 
    public synchronized void produce(int value) {
 
        while (available) {
            try {
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }
        }
 
        this.value = value;
        available = true;
 
        System.out.println("Produced: " + value);
 
        notifyAll();
    }
 
    public synchronized int consume() {
 
        while (!available) {
            try {
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return -1;
            }
        }
 
        int value = this.value;
        available = false;
 
        System.out.println("Consumed: " + value);
 
        notifyAll();
 
        return value;
    }
}
 
public class HelloWorld {
 
    public static void main(String[] args) {
 
        SharedData data = new SharedData();
 
        Thread producer = new Thread(() -> {
 
            for (int i = 1; i <= 5; i++) {
                data.produce(i);
            }
 
        });
 
        Thread consumer = new Thread(() -> {
 
            for (int i = 1; i <= 5; i++) {
                data.consume();
            }
 
        });
 
        producer.start();
        consumer.start();
    }
}

Possible output:

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

The exact scheduling and output order can vary.

Why Use while Instead of if with wait()?

This is a very important rule.

Prefer:

while (!available) {
    wait();
}

instead of:

if (!available) {
    wait();
}

A thread that wakes up must recheck the condition before proceeding.

There are several reasons for this, including:

  • Another thread may have changed the shared state before the awakened thread reacquires the monitor.
  • notifyAll() can wake multiple threads, but only one may successfully proceed first.
  • Java permits spurious wakeups.

Therefore, the condition should normally be checked in a loop.

The general pattern is:

synchronized (lock) {
 
    while (!condition) {
        lock.wait();
    }
 
    // Continue when condition is satisfied
}

Spurious Wakeups

A waiting thread can sometimes return from wait() without receiving the notification that the application expected.

This is known as a spurious wakeup.

Therefore, code should not assume:

wait();

means the desired condition is definitely true afterward.

Instead, always recheck the condition:

while (!condition) {
    wait();
}

This is one of the main reasons while is preferred over if.

Inter-Thread Communication Flow

Consider a consumer waiting for data.

Consumer Thread
      ↓
Check condition
      ↓
Data unavailable
      ↓
wait()
      ↓
Releases monitor
      ↓
WAITING

The producer then produces data:

Producer Thread
      ↓
Acquires monitor
      ↓
Produces data
      ↓
notifyAll()
      ↓
Releases monitor

The consumer becomes eligible to continue:

Consumer
   ↓
Reacquires monitor
   ↓
Checks condition again
   ↓
Processes data

The important point is that notification does not directly transfer the monitor to the waiting thread.

The awakened thread must reacquire the monitor before continuing.

wait() with Timeout

Java also provides a timed version of wait():

wait(2000);

This means the thread can wait for up to approximately 2 seconds.

Example

synchronized (lock) {
 
    while (!condition) {
        lock.wait(2000);
    }
 
}

The thread may resume earlier if it is notified, interrupted, or if the waiting period expires.

Unlike plain wait(), timed waiting places the thread in the TIMED_WAITING state.

wait() vs sleep()

wait() and sleep() are frequently confused.

wait()sleep()
Defined in ObjectDefined in Thread
Used for thread coordinationUsed to pause a thread for a period
Releases the monitor of the object on which it waitsDoes not release monitors held by the sleeping thread
Must be called while owning that object's monitorDoes not require a monitor
Can wait indefinitely or for a specified timeAlways specifies a duration
Can be paired with notify() / notifyAll()Notified by notify() is unrelated to sleep()

Example

synchronized (lock) {
    lock.wait();
}

releases lock's monitor. But:

synchronized (lock) {
    Thread.sleep(2000);
}

does not release lock's monitor.

notify() Does Not Release the Lock Immediately

Consider:

synchronized (lock) {
    lock.notify();
}

Calling notify() wakes a waiting thread, but the notifying thread still owns the monitor until it exits the synchronized block or otherwise releases the monitor.

The notified thread must wait to reacquire the monitor.

Conceptually:

Thread-1
   ↓
owns monitor
   ↓
notify()
   ↓
Thread-2 becomes eligible
   ↓
Thread-1 continues
   ↓
Thread-1 exits synchronized block
   ↓
Thread-2 can acquire monitor

notify() vs notifyAll() Example

Suppose three threads are waiting:

Thread-1 → WAITING
Thread-2 → WAITING
Thread-3 → WAITING

If we call:

notify();

one waiting thread is selected to be notified.

Conceptually:

Thread-1 → notified
Thread-2 → WAITING
Thread-3 → WAITING

If we call:

notifyAll();

all three become eligible to compete for the monitor:

Thread-1 → eligible
Thread-2 → eligible
Thread-3 → eligible

They do not all enter the synchronized section at the same time.

Inter-Thread Communication Using a Shared Lock

A common pattern is to create a dedicated lock object:

class SharedData {
 
    private final Object lock = new Object();
 
    public void waitForData() throws InterruptedException {
 
        synchronized (lock) {
 
            while (!isDataAvailable()) {
                lock.wait();
            }
 
            // Process data
        }
    }
 
    public void signalDataAvailable() {
 
        synchronized (lock) {
            // Update shared state
            lock.notifyAll();
        }
    }
 
    private boolean isDataAvailable() {
        return true;
    }
}

The same lock object is used for protecting the shared state, waiting, and notification.

Using a common monitor is essential for this communication pattern.

Thread States During Inter-Thread Communication

Inter-thread communication commonly involves these thread states.

WAITING

When:

wait();

is called without a timeout. The thread enters WAITING.

TIMED_WAITING

When:

wait(2000);

is called. The thread enters TIMED_WAITING.

BLOCKED

After a waiting thread is notified, it must reacquire the monitor.

If another thread currently owns the monitor, the notified thread may enter BLOCKED while waiting to acquire it.

Conceptually:

RUNNABLE
   ↓
wait()
   ↓
WAITING
   ↓
notify()
   ↓
BLOCKED
   ↓
acquires monitor
   ↓
RUNNABLE

This is an important part of understanding how wait() and notify() work.

wait() and Interruption

A thread waiting with wait() can be interrupted.

Example

try {
 
    synchronized (lock) {
        lock.wait();
    }
 
} catch (InterruptedException e) {
 
    Thread.currentThread().interrupt();
}

When a waiting thread is interrupted, wait() throws InterruptedException.

A common practice is to restore the thread's interrupt status when the code cannot handle the interruption immediately:

Thread.currentThread().interrupt();

Important Rule: wait() Is Not a Thread Method

A common misconception is that wait() belongs to Thread.

It does not. wait() is defined in:

java.lang.Object

Therefore, it can be called on an object whose monitor is being used:

lock.wait();

Similarly:

lock.notify();
lock.notifyAll();

are methods of Object.

wait(), notify(), and notifyAll() Belong to Object

The methods are defined in the Object class:

public final void wait()
public final native void wait(long timeoutMillis)
public final void notify()
public final native void notifyAll()

The exact low-level implementation details are JVM-specific, but from a Java programming perspective, these methods are used with an object's monitor for thread coordination.

Common Mistakes

Mistake 1: Calling wait() Outside Synchronization

Incorrect:

lock.wait();

Correct:

synchronized (lock) {
    lock.wait();
}

Otherwise, IllegalMonitorStateException can occur.

Mistake 2: Calling notify() on a Different Object

Suppose one thread waits using:

synchronized (lock1) {
    lock1.wait();
}

Another thread must notify using the same monitor:

synchronized (lock1) {
    lock1.notify();
}

Using another object:

synchronized (lock2) {
    lock2.notify();
}

does not notify the thread waiting on lock1.

Mistake 3: Using if Instead of while

Avoid:

if (!condition) {
    wait();
}

Prefer:

while (!condition) {
    wait();
}

The condition must be checked again after the thread wakes up.

Mistake 4: Assuming notify() Immediately Runs the Waiting Thread

notify() only makes one waiting thread eligible to continue.

The notifying thread still owns the monitor until it releases it.

Mistake 5: Assuming notifyAll() Runs All Threads Together

notifyAll() wakes all waiting threads, but only one thread can own a particular monitor at a time.

The awakened threads compete to reacquire the monitor.

Inter-Thread Communication vs Synchronization

These concepts are related but not identical.

Synchronization

Synchronization controls access to shared resources.

synchronized (lock) {
    // protected code
}

Inter-Thread Communication

Inter-thread communication allows threads to coordinate based on conditions.

synchronized (lock) {
 
    while (!condition) {
        lock.wait();
    }
 
    lock.notifyAll();
}

A simple way to remember:

Synchronization
      ↓
Controls access
 
Inter-thread communication
      ↓
Coordinates threads

Inter-thread communication commonly relies on synchronization and monitors when using wait(), notify(), and notifyAll().

Producer-Consumer Concept

The producer-consumer problem is one of the most common examples of inter-thread communication.

         Producer
             |
             | produces
             v
      +---------------+
      | Shared Buffer |
      +---------------+
             |
             | consumes
             v
          Consumer

The producer waits when the buffer is full.

The consumer waits when the buffer is empty.

When the state changes, the corresponding thread can notify waiting threads.

For real applications, Java's concurrency utilities such as BlockingQueue often provide a safer and simpler implementation of producer-consumer coordination.

Using BlockingQueue

Instead of manually implementing wait() and notify(), Java provides higher-level concurrency utilities.

For example:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ArrayBlockingQueue;
 
public class HelloWorld {
 
    public static void main(String[] args) {
 
        BlockingQueue<Integer> queue =
                new ArrayBlockingQueue<>(10);
 
        Thread producer = new Thread(() -> {
 
            try {
                queue.put(100);
                System.out.println("Produced: 100");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
 
        Thread consumer = new Thread(() -> {
 
            try {
                int value = queue.take();
                System.out.println("Consumed: " + value);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
 
        producer.start();
        consumer.start();
    }
}

Here:

put()
 ↓
adds data
 
take()
 ↓
removes data

BlockingQueue handles the waiting and notification mechanics internally.

This is often preferable to implementing a producer-consumer queue manually.

When Should You Use wait(), notify(), and notifyAll()?

These methods are useful for understanding Java's monitor-based thread communication and can be appropriate in carefully designed low-level synchronization code.

However, for many production applications, higher-level concurrency utilities are preferable.

Examples include:

  • BlockingQueue
  • ExecutorService
  • Future
  • CompletableFuture
  • CountDownLatch
  • Semaphore
  • CyclicBarrier
  • Phaser
  • Lock and Condition

These abstractions can make concurrent programs easier to design and maintain.

Advantages of Inter-Thread Communication

  • Allows threads to coordinate their activities.
  • Avoids unnecessary busy waiting.
  • Helps implement producer-consumer patterns.
  • Allows a thread to wait until a required condition becomes true.
  • Provides a mechanism for notifying waiting threads.
  • Can improve CPU efficiency compared with continuous polling.

Limitations and Risks

  • Incorrect use can cause deadlocks.
  • Incorrect condition handling can cause missed coordination or threads waiting indefinitely.
  • Using if instead of while around wait() can lead to incorrect behavior.
  • notify() may wake a thread that cannot currently make progress.
  • Complex communication logic can become difficult to maintain.
  • Higher-level concurrency utilities are often easier to use safely.

Key Takeaways

  • Inter-thread communication allows threads to coordinate with each other.
  • Java provides wait(), notify(), and notifyAll() for traditional monitor-based communication.
  • These methods are defined in the Object class.
  • wait() releases the relevant object's monitor while the thread waits.
  • notify() wakes one waiting thread.
  • notifyAll() wakes all threads waiting on the same monitor.
  • The notified thread must reacquire the monitor before continuing.
  • wait(), notify(), and notifyAll() must be called while owning the relevant object's monitor.
  • Always recheck a waiting condition using a while loop.
  • sleep() does not release a monitor.
  • wait() can result in WAITING or TIMED_WAITING.
  • A notified thread may temporarily enter BLOCKED while waiting to reacquire the monitor.
  • BlockingQueue is often a better choice for producer-consumer applications than implementing wait() and notify() manually.

Simple Concept to Remember

         Thread-1
             |
          wait()
             |
             v
          WAITING
             |
             |
        notify/notifyAll
             |

v Reacquire monitor | v Continue

The key idea is: Inter-thread communication allows threads to wait for conditions and notify each other when the shared state changes.

See you in next page....🙂

← SynchronizationDeadlock →
← SynchronizationDeadlock →