A thread does not simply start and stop; it moves through multiple states based on system conditions, synchronization, and scheduling decisions. These transitions are what define the behavior and performance of multithreaded applications.
State Transitions
A thread in Java moves through different states during its lifecycle. These states are not isolated—they are connected through well-defined transitions triggered by method calls, resource availability, or system events.NEW → RUNNABLE
A thread begins in the NEW state when it is created but not yet started. Oncestart() is invoked, the thread transitions to the RUNNABLE state.
Thread t = new Thread(() -> {
System.out.println("Running...");
});
t.start();
At this point, the thread is ready to execute, but it depends on the scheduler to actually run.
RUNNABLE → RUNNING (Implicit)
Java does not explicitly define a RUNNING state, but when a thread is actively executing, it is considered to be running within the RUNNABLE state.The transition from ready-to-run to actively running is controlled by the thread scheduler.
RUNNABLE → BLOCKED
A thread enters the BLOCKED state when it attempts to acquire a lock that is already held by another thread. synchronized (this) {
// thread needs lock to enter here
}
The thread remains blocked until the lock becomes available.
RUNNABLE → WAITING
A thread enters the WAITING state when it waits indefinitely for another thread to perform a specific action. synchronized (obj) {
obj.wait();
}
It will remain in this state until it receives a notification via notify() or notifyAll().
RUNNABLE → TIMED_WAITING
A thread enters the TIMED_WAITING state when it waits for a specified duration.Thread.sleep(1000);
After the timeout expires, the thread returns to the RUNNABLE state.
WAITING/TIMED_WAITING → RUNNABLE
A waiting thread becomes runnable again when:- It is notified (
notify() / notifyAll())- The timeout expires
- It is interrupted
BLOCKED → RUNNABLE
A blocked thread transitions back to RUNNABLE when it successfully acquires the required lock.RUNNABLE → TERMINATED
A thread enters the TERMINATED state when itsrun() method completes execution.
Thread t = new Thread(() -> {
System.out.println("Task completed");
});
t.start();
Once terminated, a thread cannot be restarted.
Thread Scheduler
The thread scheduler is a crucial component responsible for deciding which thread gets CPU time. It is part of the underlying operating system, not the JVM itself, although the JVM interacts closely with it.When multiple threads are in the RUNNABLE state, the scheduler selects one of them for execution based on various factors such as priority, availability, and scheduling policy.
Scheduling Strategies
Two common scheduling approaches are used:1. Preemptive Scheduling
In this approach, the scheduler can interrupt a running thread and allocate CPU time to another thread. Most modern operating systems use preemptive scheduling.2. Time-Slicing
Each thread is given a small time slice (quantum) to execute. After its time expires, the CPU switches to another thread.Thread Priority
Java allows threads to have priorities ranging fromThread.MIN_PRIORITY (1) to Thread.MAX_PRIORITY (10).
Thread t = new Thread(() -> {
System.out.println("Running with priority");
});
t.setPriority(Thread.MAX_PRIORITY);
t.start();
However, thread priority is only a hint to the scheduler and may not always be strictly followed depending on the OS.
The exact behavior of thread scheduling is not guaranteed and may vary across platforms. Therefore, developers should never rely on thread scheduling order for correctness.
Context Switching
Context switching is the process of saving the state of one thread and restoring the state of another so that multiple threads can share a single CPU effectively.What Happens During Context Switch?
When the CPU switches from one thread to another, it must:- Save the current thread's state (registers, program counter, stack)
- Load the next thread's state
- Resume execution from where the new thread left off
This allows multiple threads to appear as if they are running simultaneously, even on a single-core processor.
Performance Impact
Context switching is not free—it has overhead. Excessive context switching can degrade performance because the CPU spends more time switching between threads than doing actual work.Example Scenario
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName());
}).start();
}
In this example, multiple threads compete for CPU time. The scheduler rapidly switches between them, creating the illusion of parallel execution.
Key Insights
- Threads constantly move between states based on conditions and system behavior- The scheduler controls execution but is not predictable
- Context switching enables concurrency but introduces overhead
- Efficient thread management is critical for performance
A deep understanding of thread state transitions, scheduling, and context switching is essential for building high-performance concurrent applications. These concepts explain why threads behave the way they do and help developers avoid common pitfalls such as performance bottlenecks and unpredictable execution.
In the next chapter, we will explore core thread methods such as
sleep(), join(), and interrupt(), which give developers fine-grained control over thread execution.
Join the discussion