Java Thread States and Transitions: Sleep, Wait, Notify, Priority and Yielding

🔰 Introduction to Thread States and Control Mechanisms

Welcome to this comprehensive guide on Java thread states and control mechanisms! Threading is one of the most powerful yet challenging aspects of Java programming. Understanding how threads transition between different states and how to control their behavior is essential for writing efficient, bug-free concurrent applications.

In this tutorial, we'll explore the intricate dance of thread state transitions, focusing specifically on the mechanisms that cause these transitions: sleep(), wait(), notify(), thread priorities, and yielding. These tools give you fine-grained control over thread execution, allowing you to orchestrate complex multi-threaded applications.

Whether you're building responsive user interfaces, high-throughput server applications, or real-time systems, mastering thread states and transitions will help you write more efficient, reliable, and maintainable code. Let's dive in!

🧠 Thread States and Transitions

Thread State Transitions

Now, let's explore the key mechanisms that cause threads to transition between states:

1. 💤 Sleep Mechanism (Thread.sleep())

The sleep() method causes the current thread to suspend execution for a specified period:

Thread.sleep(milliseconds);
// or
Thread.sleep(milliseconds, nanoseconds);

State Transition: RUNNABLE → TIMED_WAITING

When a thread calls sleep():

  • It temporarily ceases execution for the specified duration
  • It does not release any locks it holds
  • After the sleep time elapses, it returns to RUNNABLE state (not necessarily running immediately)
  • If interrupted during sleep, it throws InterruptedException

2. 🔒 Wait-Notify Mechanism (wait(), notify(), notifyAll())

These methods are used for inter-thread communication and are called on an object, not on a thread:

synchronized(lockObject) {
    lockObject.wait();       // Makes thread wait indefinitely
    lockObject.wait(timeout); // Makes thread wait for specified time
    
    lockObject.notify();     // Wakes up a single waiting thread
    lockObject.notifyAll();  // Wakes up all waiting threads
}

State Transitions:

  • wait(): RUNNABLE → WAITING
  • wait(timeout): RUNNABLE → TIMED_WAITING
  • notify()/notifyAll(): WAITING/TIMED_WAITING → RUNNABLE (for the notified thread)

Important characteristics:

  • These methods must be called from within a synchronized block/method
  • When a thread calls wait(), it releases the lock on the object
  • When notified, a thread must reacquire the lock before continuing execution

3. ⚖️ Thread Priority and Yielding

Thread priorities influence (but don't guarantee) the order in which threads are scheduled:

thread.setPriority(Thread.MIN_PRIORITY);   // 1
thread.setPriority(Thread.NORM_PRIORITY);  // 5 (default)
thread.setPriority(Thread.MAX_PRIORITY);   // 10

The yield() method suggests that the current thread is willing to yield its current use of the processor:

Thread.yield();

State Transition: None directly, but affects scheduling within the RUNNABLE state

Key points:

  • Priorities range from 1 (lowest) to 10 (highest)
  • Higher priority threads are generally scheduled before lower priority ones
  • Thread scheduling is platform-dependent and not guaranteed
  • yield() is a hint to the scheduler, not a command

🔄 The Complete Thread Lifecycle

Let's put it all together to understand the complete thread lifecycle:

  1. Creation: Thread object is created → NEW state
  2. Starting: start() method is called → RUNNABLE state
  3. Running: Thread is executing → Still in RUNNABLE state
  4. Non-Running: Thread is not currently executing but ready to run → Still in RUNNABLE state
  5. Blocking Operations:
    • Waiting for a lock → BLOCKED state
    • Calling wait() → WAITING state
    • Calling sleep() or join() → TIMED_WAITING state
  6. Termination: Thread completes execution → TERMINATED state

💻 Complete Example with Code Comments

Let's create a comprehensive example that demonstrates all the thread states and transitions we've discussed:

public class ThreadStatesDemo {
    private static final Object lock = new Object();
    
    public static void main(String[] args) throws InterruptedException {
        // Create a thread that will demonstrate various states
        Thread testThread = new Thread(() -> {
            try {
                // Demonstrate TIMED_WAITING state with sleep
                System.out.println("Thread going to sleep...");
                Thread.sleep(2000);
                
                // Demonstrate BLOCKED state
                System.out.println("Thread trying to acquire lock...");
                synchronized(lock) {
                    System.out.println("Thread acquired lock");
                    
                    // Demonstrate WAITING state
                    System.out.println("Thread going to wait...");
                    lock.wait();
                    System.out.println("Thread resumed after notification");
                }
                
                // Demonstrate yielding
                for (int i = 0; i < 5; i++) {
                    System.out.println("Thread counting: " + i);
                    if (i == 2) {
                        System.out.println("Thread yielding...");
                        Thread.yield(); // Hint to scheduler to let other threads run
                    }
                }
            } catch (InterruptedException e) {
                System.out.println("Thread was interrupted");
                return; // Exit the thread
            }
        }, "TestThread");
        
        // Create a monitor thread to observe state changes
        Thread monitorThread = new Thread(() -> {
            Thread.State oldState = testThread.getState();
            System.out.println("TestThread initial state: " + oldState);
            
            while (true) {
                Thread.State state = testThread.getState();
                if (state != oldState) {
                    System.out.println("TestThread state changed from " + oldState + " to " + state);
                    oldState = state;
                }
                
                if (state == Thread.State.TERMINATED) {
                    System.out.println("TestThread final state: " + state);
                    break;
                }
                
                try {
                    Thread.sleep(100); // Check state every 100ms
                } catch (InterruptedException e) {
                    break;
                }
            }
        }, "MonitorThread");
        
        // Start the monitor thread first
        monitorThread.start();
        
        // Wait a bit to let monitor thread start
        Thread.sleep(500);
        
        // Start the test thread
        testThread.start();
        
        // Main thread acquires lock to force test thread into BLOCKED state
        synchronized(lock) {
            System.out.println("Main thread holding the lock...");
            Thread.sleep(2000); // Hold lock for 2 seconds
            
            // Release lock, allowing test thread to acquire it and enter WAITING state
        }
        
        // Wait for test thread to enter WAITING state
        Thread.sleep(500);
        
        // Notify the waiting thread
        synchronized(lock) {
            System.out.println("Main thread notifying...");
            lock.notify();
        }
        
        // Wait for both threads to finish
        testThread.join();
        monitorThread.join();
        
        System.out.println("Demo completed");
    }
}

Expected Output:

TestThread initial state: NEW
TestThread state changed from NEW to RUNNABLE
Thread going to sleep...
TestThread state changed from RUNNABLE to TIMED_WAITING
Main thread holding the lock...
TestThread state changed from TIMED_WAITING to RUNNABLE
Thread trying to acquire lock...
TestThread state changed from RUNNABLE to BLOCKED
Main thread notifying...
TestThread state changed from BLOCKED to RUNNABLE
Thread acquired lock
Thread going to wait...
TestThread state changed from RUNNABLE to WAITING
TestThread state changed from WAITING to RUNNABLE
Thread resumed after notification
Thread counting: 0
Thread counting: 1
Thread counting: 2
Thread yielding...
Thread counting: 3
Thread counting: 4
TestThread state changed from RUNNABLE to TERMINATED
TestThread final state: TERMINATED
Demo completed

Code Explanation:

  • The example creates two threads: a test thread that goes through various state transitions and a monitor thread that observes these transitions.
  • The monitor thread uses getState() to check the test thread's state every 100ms and reports any changes.
  • The main thread orchestrates the transitions by controlling when it holds the lock and when it notifies the waiting thread.
  • We can observe all major state transitions: NEW → RUNNABLE → TIMED_WAITING → RUNNABLE → BLOCKED → RUNNABLE → WAITING → RUNNABLE → TERMINATED.

📦 More Practical Code Snippets

1. Producer-Consumer Pattern with Wait-Notify

public class ProducerConsumerExample {
    private static final int BUFFER_SIZE = 5;
    private final Queue<Integer> buffer = new LinkedList<>();
    private final Object lock = new Object();
    
    public void produce() throws InterruptedException {
        int value = 0;
        while (true) {
            synchronized (lock) {
                // Wait while buffer is full
                while (buffer.size() == BUFFER_SIZE) {
                    System.out.println("Buffer full, producer waiting...");
                    lock.wait(); // RUNNABLE → WAITING
                }
                
                // Produce a new item
                value++;
                buffer.add(value);
                System.out.println("Produced: " + value);
                
                // Notify consumer
                lock.notifyAll();
            }
            
            // Simulate work
            Thread.sleep((int)(Math.random() * 1000));
        }
    }
    
    public void consume() throws InterruptedException {
        while (true) {
            synchronized (lock) {
                // Wait while buffer is empty
                while (buffer.isEmpty()) {
                    System.out.println("Buffer empty, consumer waiting...");
                    lock.wait(); // RUNNABLE → WAITING
                }
                
                // Consume an item
                int value = buffer.poll();
                System.out.println("Consumed: " + value);
                
                // Notify producer
                lock.notifyAll();
            }
            
            // Simulate work
            Thread.sleep((int)(Math.random() * 1000));
        }
    }
    
    public static void main(String[] args) {
        ProducerConsumerExample example = new ProducerConsumerExample();
        
        Thread producerThread = new Thread(() -> {
            try {
                example.produce();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        
        Thread consumerThread = new Thread(() -> {
            try {
                example.consume();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        
        producerThread.start();
        consumerThread.start();
    }
}

2. Thread Priority Example

public class ThreadPriorityExample {
    public static void main(String[] args) {
        // Create counter threads with different priorities
        Counter lowPriorityCounter = new Counter("Low Priority");
        Counter normalPriorityCounter = new Counter("Normal Priority");
        Counter highPriorityCounter = new Counter("High Priority");
        
        // Set different priorities
        lowPriorityCounter.setPriority(Thread.MIN_PRIORITY); // 1
        // normalPriorityCounter uses default priority (5)
        highPriorityCounter.setPriority(Thread.MAX_PRIORITY); // 10
        
        // Start all threads
        lowPriorityCounter.start();
        normalPriorityCounter.start();
        highPriorityCounter.start();
        
        // Let them run for a while
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        // Stop all threads
        lowPriorityCounter.stopCounting();
        normalPriorityCounter.stopCounting();
        highPriorityCounter.stopCounting();
        
        // Print results
        System.out.println("\nFinal counts after 5 seconds:");
        System.out.println(lowPriorityCounter.getName() + ": " + lowPriorityCounter.getCount());
        System.out.println(normalPriorityCounter.getName() + ": " + normalPriorityCounter.getCount());
        System.out.println(highPriorityCounter.getName() + ": " + highPriorityCounter.getCount());
    }
    
    static class Counter extends Thread {
        private long count = 0;
        private volatile boolean running = true;
        
        public Counter(String name) {
            super(name);
        }
        
        public void run() {
            while (running) {
                count++;
                
                // Yield every 1000 counts
                if (count % 1000 == 0) {
                    Thread.yield();
                }
            }
        }
        
        public void stopCounting() {
            running = false;
        }
        
        public long getCount() {
            return count;
        }
    }
}

3. Timed Wait with Timeout Recovery

public class TimedWaitExample {
    private static final Object lock = new Object();
    private static boolean signalReceived = false;
    
    public static void main(String[] args) {
        Thread waiterThread = new Thread(() -> {
            synchronized (lock) {
                System.out.println("Waiter: Waiting for signal...");
                
                try {
                    long startTime = System.currentTimeMillis();
                    
                    // Wait for up to 5 seconds for a signal
                    lock.wait(5000); // RUNNABLE → TIMED_WAITING
                    
                    long elapsedTime = System.currentTimeMillis() - startTime;
                    
                    if (signalReceived) {
                        System.out.println("Waiter: Signal received after " + elapsedTime + "ms");
                    } else {
                        System.out.println("Waiter: Timed out after " + elapsedTime + "ms");
                        // Perform recovery actions here
                        System.out.println("Waiter: Performing timeout recovery...");
                    }
                } catch (InterruptedException e) {
                    System.out.println("Waiter: Interrupted while waiting");
                    Thread.currentThread().interrupt();
                }
            }
        });
        
        Thread signalerThread = new Thread(() -> {
            // Simulate some work
            try {
                // Decide whether to signal or not (randomly)
                boolean shouldSignal = Math.random() > 0.5;
                
                if (shouldSignal) {
                    // Wait for a random time between 1-3 seconds
                    int delay = 1000 + (int)(Math.random() * 2000);
                    System.out.println("Signaler: Will signal after " + delay + "ms");
                    Thread.sleep(delay);
                    
                    synchronized (lock) {
                        signalReceived = true;
                        System.out.println("Signaler: Sending signal");
                        lock.notify();
                    }
                } else {
                    System.out.println("Signaler: Decided not to signal (testing timeout)");
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        
        waiterThread.start();
        signalerThread.start();
    }
}

🚀 Why It Matters / Use Cases

Understanding thread states and transitions is crucial for several reasons:

1. Resource Efficiency

By properly managing thread states, you can:

  • Reduce CPU usage by putting threads to sleep when they're not needed
  • Free up resources by allowing waiting threads to release locks
  • Improve throughput by prioritizing important tasks

2. Coordination and Synchronization

Thread state control mechanisms enable:

  • Producer-consumer patterns where producers wait when buffers are full and consumers wait when buffers are empty
  • Worker pools where threads wait for tasks and are notified when work is available
  • Barrier patterns where threads wait for all other threads to reach a certain point

3. Responsiveness

In UI applications:

  • Long-running tasks can be moved to background threads to keep the UI responsive
  • Thread priorities can ensure UI threads get preferential treatment
  • Sleep and yield can be used to give other threads a chance to run

4. Real-World Use Cases

Web Servers

  • Thread pools with workers that wait for incoming connections
  • Priority-based scheduling for different types of requests

Database Systems

  • Connection pooling with threads that sleep when idle
  • Transaction coordination with wait-notify mechanisms

Real-Time Systems

  • Priority-based scheduling for time-critical operations
  • Precise timing control with sleep and wait mechanisms

Parallel Processing

  • Work-stealing algorithms using thread priorities
  • Coordination of parallel tasks with wait-notify

🧭 Best Practices / Rules to Follow

1. Sleep Best Practices

DO:

  • Use sleep for timing and pacing operations
  • Always catch InterruptedException and restore the interrupt status
  • Consider using higher-level abstractions like ScheduledExecutorService for timing

DON'T:

  • Use sleep for synchronization between threads
  • Sleep while holding locks for long periods
  • Use arbitrary sleep times for race condition fixes
// GOOD: Proper sleep with interrupt handling
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    // Restore the interrupted status
    Thread.currentThread().interrupt();
    // Handle the interruption appropriately
}

// BAD: Ignoring interruption
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    // Do nothing - BAD PRACTICE!
}

2. Wait-Notify Best Practices

DO:

  • Always call wait() in a loop that checks the condition
  • Always call wait() and notify() from synchronized blocks on the same object
  • Prefer notifyAll() over notify() unless you're certain which thread should wake up

DON'T:

  • Call wait() outside of a synchronized block
  • Assume a thread will still hold a condition true after being notified
  • Use wait/notify for simple timing (use sleep instead)
// GOOD: Wait with condition check in a loop
synchronized (lock) {
    while (!condition) {  // Use while, not if
        lock.wait();
    }
    // Process after condition is true
}

// BAD: Wait without proper synchronization or condition check
if (!condition) {  // Using if instead of while
    lock.wait();  // May throw IllegalMonitorStateException or miss notifications
}

3. Thread Priority Best Practices

DO:

  • Use priorities sparingly and only when necessary
  • Keep most threads at normal priority
  • Test your application on different platforms

DON'T:

  • Rely on priorities for correctness (they're just hints)
  • Set all threads to MAX_PRIORITY (defeats the purpose)
  • Use priorities to fix race conditions or timing issues
// GOOD: Selective use of priorities
uiThread.setPriority(Thread.MAX_PRIORITY);
backgroundThread.setPriority(Thread.MIN_PRIORITY);

// BAD: Setting all threads to high priority
thread1.setPriority(Thread.MAX_PRIORITY);
thread2.setPriority(Thread.MAX_PRIORITY);
thread3.setPriority(Thread.MAX_PRIORITY);
// Now priorities are meaningless

4. Yielding Best Practices

DO:

  • Use yield() as a hint only, not for synchronization
  • Consider yield() for CPU-intensive loops to improve responsiveness

DON'T:

  • Rely on yield() for thread coordination
  • Expect consistent behavior across different JVMs or platforms
  • Use yield() to fix concurrency bugs
// GOOD: Using yield in a CPU-intensive loop
while (processing) {
    // Do some CPU-intensive work
    
    if (++iterations % 100 == 0) {
        Thread.yield(); // Give other threads a chance occasionally
    }
}

// BAD: Using yield for synchronization
while (!sharedData.isReady()) {
    Thread.yield(); // Busy waiting - very inefficient
}

5. General Thread State Management

DO:

  • Use higher-level concurrency utilities when possible (java.util.concurrent)
  • Always handle InterruptedException properly
  • Design for thread safety from the beginning

DON'T:

  • Use Thread.stop(), Thread.suspend(), or Thread.resume() (deprecated)
  • Create too many threads (use thread pools instead)
  • Assume thread scheduling behavior will be the same across all platforms

⚠️ Common Pitfalls or Gotchas

1. The Spurious Wakeup Problem

Threads can wake up from wait() without being notified! This is why you should always use a while loop to check conditions:

// CORRECT: Guards against spurious wakeups
synchronized (lock) {
    while (!condition) {
        lock.wait();
    }
    // Safe to proceed
}

// INCORRECT: Vulnerable to spurious wakeups
synchronized (lock) {
    if (!condition) {
        lock.wait();
    }
    // Might proceed when condition is still false!
}

2. Deadlocks with Wait-Notify

If all threads are waiting and none are left to call notify(), you have a deadlock:

// Potential deadlock scenario
synchronized (lockA) {
    synchronized (lockB) {
        lockA.wait(); // Waiting for notification that will never come
                     // if all threads are waiting
    }
}

3. The Lost Notification Problem

If notify() is called before wait(), the notification is lost:

// Thread A
synchronized (lock) {
    // Condition is already true
    lock.notify(); // Notification sent, but no one is waiting yet
}

// Thread B (later)
synchronized (lock) {
    while (!condition) {
        lock.wait(); // Will wait indefinitely - notification was lost
    }
}

4. IllegalMonitorStateException

Calling wait(), notify(), or notifyAll() without owning the monitor:

// Will throw IllegalMonitorStateException
Object lock = new Object();
lock.wait(); // Not in a synchronized block!

// Correct way
synchronized (lock) {
    lock.wait();
}

5. Priority Inversion

A low-priority thread holds a lock needed by a high-priority thread, but a medium-priority thread prevents the low-priority thread from releasing the lock:

// Low priority thread
synchronized (lock) {
    // Critical section
    // ... gets preempted by medium priority thread before it can finish
}

// High priority thread
synchronized (lock) {
    // Blocked waiting for low priority thread
    // But medium priority thread keeps running!
}

6. Sleep Doesn't Release Locks

Unlike wait(), sleep() doesn't release locks, which can cause unexpected blocking:

synchronized (lock) {
    // Do something
    Thread.sleep(10000); // Holds the lock for 10 seconds!
    // Other threads can't acquire the lock during sleep
}

7. Interrupting a Thread Doesn't Stop It

Interrupting only sets the interrupted flag; the thread must check and respond:

// Thread code
while (!Thread.currentThread().isInterrupted()) {
    // Work
}

// Or when using blocking methods
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    // Important: InterruptedException clears the interrupt flag!
    Thread.currentThread().interrupt(); // Restore the flag
    return; // Exit the thread
}

📌 Summary / Key Takeaways

  • Thread States: Java threads can be in one of six states: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, or TERMINATED.

  • Sleep Mechanism:

    • Causes thread to enter TIMED_WAITING state
    • Does not release locks
    • Useful for timing and pacing operations
    • Must handle InterruptedException
  • Wait-Notify Mechanism:

    • wait() causes thread to enter WAITING or TIMED_WAITING state
    • Releases the lock on the object
    • Must be called from synchronized context
    • Always use wait() in a loop checking the condition
    • notify() wakes one thread, notifyAll() wakes all waiting threads
  • Thread Priority:

    • Values range from 1 (MIN_PRIORITY) to 10 (MAX_PRIORITY)
    • Only influences scheduling, doesn't guarantee execution order
    • Platform-dependent behavior
    • Use sparingly and don't rely on for correctness
  • Thread Yielding:

    • yield() is a hint to the scheduler
    • Suggests that the thread is willing to give up its current timeslice
    • No guarantees about which thread will run next
    • Don't use for synchronization or coordination
  • Best Practices:

    • Always handle InterruptedException properly
    • Use wait() in loops that check conditions
    • Prefer higher-level concurrency utilities when possible
    • Don't rely on thread priorities for correctness
    • Be aware of platform-dependent scheduling behavior
  • Common Pitfalls:

    • Spurious wakeups from wait()
    • Deadlocks with wait-notify
    • Lost notifications
    • IllegalMonitorStateException
    • Priority inversion
    • Sleep holding locks
    • Mishandling thread interruption

🧩 Exercises or Mini-Projects

Exercise 1: Thread State Observer

Create a program that spawns multiple threads performing different tasks (sleeping, waiting, computing) and implements an observer thread that monitors and reports their states in real-time.

Requirements:

  • Create at least 3 worker threads with different behaviors
  • Implement a monitor thread that reports state changes
  • Display a summary of how much time each thread spent in each state

Exercise 2: Producer-Consumer with Timeout

Implement a producer-consumer pattern with multiple producers and consumers using wait-notify. Add a timeout mechanism so consumers don't wait indefinitely if no items are produced.

Requirements:

  • Use wait(timeout) for consumers
  • Implement proper shutdown mechanism
  • Handle the case where consumers time out
  • Track and report statistics (items produced/consumed, wait times)

Exercise 3: Priority-Based Task Scheduler

Create a simple task scheduler that assigns different priorities to tasks and executes them accordingly.

Requirements:

  • Define a Task interface with execute() method
  • Implement a PriorityTaskScheduler class
  • Allow tasks to be submitted with different priorities
  • Measure and compare actual execution order vs. expected order

Exercise 4: Dining Philosophers Problem

Implement the classic dining philosophers problem, using wait-notify for coordination.

Requirements:

  • Implement 5 philosopher threads and 5 fork objects
  • Use proper synchronization to prevent deadlock
  • Use wait-notify for philosophers to wait for forks
  • Add a timeout mechanism to prevent starvation

Exercise 5: Thread State Transition Game

Create an educational game that challenges players to predict thread state transitions given certain operations.

Requirements:

  • Present code snippets and ask players to identify resulting thread states
  • Include scenarios with sleep, wait, notify, and synchronization
  • Provide explanations for correct answers
  • Track player score and provide feedback

🔄 Advanced Topics for Further Exploration

  • Java Memory Model and its relationship with thread states
  • Lock-free programming techniques
  • Fork/Join Framework for parallel processing
  • CompletableFuture for asynchronous programming
  • Phaser, CountDownLatch, CyclicBarrier for advanced thread coordination
  • Atomic variables for lock-free synchronization
  • ThreadLocal for thread-confined data

By mastering thread states and transitions in Java, you'll be well-equipped to build robust, efficient concurrent applications. Remember that thread management is as much art as science - practice, experimentation, and careful design are key to success in this domain.

Happy threading!