Backend Development 15 min read

Understanding Java Thread States and Synchronization Methods: wait/notify, sleep/yield, and join

This article explains Java thread lifecycle states, demonstrates how to use wait, notify, and notifyAll for inter‑thread coordination, compares sleep, yield, and join methods with practical code examples, and highlights important considerations such as monitor ownership and thread scheduling.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Understanding Java Thread States and Synchronization Methods: wait/notify, sleep/yield, and join

Java threads can be in five states: New (created but not started), Runnable (ready to run), Running (executing), Blocked (waiting for a resource), and Dead (finished or terminated). The article shows the state transition diagram and explains how these states are implemented internally.

The wait/notify/notifyAll methods belong to Object because every object has an intrinsic monitor. wait() suspends the current thread until another thread calls notify() (wakes one waiting thread) or notifyAll() (wakes all waiting threads) on the same monitor. The article provides three overloads of wait and shows the JDK 8 source for wait(long timeout, int nanos) , noting that nanosecond precision is rounded to milliseconds.

public final void wait(long timeout, int nanos) throws InterruptedException {
    if (timeout < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }
    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException("nanosecond timeout value out of range");
    }
    if (nanos >= 500000 || (nanos != 0 && timeout == 0)) {
        timeout++;
    }
    wait(timeout);
}

A simple example demonstrates that calling wait() without holding the monitor causes IllegalMonitorStateException . Adding the synchronized keyword to the method fixes the problem, allowing the thread to pause for the specified time and then resume when notified.

package com.paddx.test.concurrent;

public class WaitTest {
    public synchronized void testWait() {
        System.out.println("Start-----");
        try {
            wait(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("End-------");
    }
    public static void main(String[] args) {
        final WaitTest test = new WaitTest();
        new Thread(() -> test.testWait()).start();
    }
}

The article then compares notify (wakes a single waiting thread) with notifyAll (wakes all waiting threads) using a NotifyTest example that creates several threads, calls notify() once, sleeps, and then calls notifyAll() , showing the different output patterns.

public class NotifyTest {
    public synchronized void testWait() {
        System.out.println(Thread.currentThread().getName() + " Start-----");
        try { wait(0); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println(Thread.currentThread().getName() + " End-------");
    }
    public static void main(String[] args) throws InterruptedException {
        final NotifyTest test = new NotifyTest();
        for (int i = 0; i < 5; i++) {
            new Thread(test::testWait).start();
        }
        synchronized (test) { test.notify(); }
        Thread.sleep(3000);
        synchronized (test) { test.notifyAll(); }
    }
}

Next, the article examines three thread‑control methods defined in Thread :

sleep : pauses the current thread for a given time without releasing the monitor.

yield : hints to the scheduler that the current thread is willing to give up the CPU, moving from RUNNING to RUNNABLE; its effect is not guaranteed.

join : makes the calling thread wait until another thread terminates; internally it uses wait() on the target thread’s monitor.

Code examples for each method illustrate their behavior, including a SleepTest that shows sleep holding the lock, a YieldTest that interleaves two threads, and JoinTest examples that compare execution without and with join() , highlighting the sequential execution when join is used.

Finally, the article answers why wait/notify/notifyAll are defined in Object rather than Thread : every object has an intrinsic monitor, so these coordination primitives are universally available to any object, enabling flexible synchronization patterns.

concurrencythreadSynchronizationJoinsleepwait-notify
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.