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 → WAITINGwait(timeout)
: RUNNABLE → TIMED_WAITINGnotify()/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:
- Creation: Thread object is created → NEW state
- Starting:
start()
method is called → RUNNABLE state - Running: Thread is executing → Still in RUNNABLE state
- Non-Running: Thread is not currently executing but ready to run → Still in RUNNABLE state
- Blocking Operations:
- Waiting for a lock → BLOCKED state
- Calling
wait()
→ WAITING state - Calling
sleep()
orjoin()
→ TIMED_WAITING state
- 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!