“Is my thread stuck or just sleeping?”
Riya's canteen app has a background thread that checks the payment status every few seconds. A customer complains the app is “stuck”. Riya stares at her code: is the thread dead, sleeping, or waiting for something?
Sir, how do I know what my thread is doing right now?
Ask it. t.getState() returns one of six states. A sleeping thread says TIMED_WAITING, a stuck one is often BLOCKED or WAITING, and a dead one says TERMINATED.
And if it's sleeping but the user wants to cancel?
Then you interrupt() it. It wakes up early and can stop cleanly. Let's go through the whole life of a thread.
What is the thread lifecycle?
The thread lifecycle is the sequence of states a thread passes through from creation to termination. In Java these are the constants of the enum Thread.State: NEW (created, not started), RUNNABLE (executing or ready to execute), BLOCKED (waiting to acquire a monitor lock), WAITING (waiting indefinitely for another thread, e.g. join(), wait()), TIMED_WAITING (waiting for a specified time, e.g. sleep(ms), join(ms)) and TERMINATED (completed). A thread's current state is returned by getState().
Like a delivery rider's day: hired (NEW), on the road or waiting at a signal (RUNNABLE), on a timed break (TIMED_WAITING), waiting for the restaurant to hand over food (WAITING), queued at the one-person counter (BLOCKED), and shift over (TERMINATED).
Words you'll meet in this lesson
start() and termination.The six states at a glance
| State | How it gets there | How it leaves |
|---|---|---|
NEW | new Thread(…) | start() |
RUNNABLE | start(), or a wait ends | a wait, a lock, or run() ends |
TIMED_WAITING | sleep(ms), join(ms), wait(ms) | time up, or interrupted |
WAITING | join(), wait() | other thread ends / notifies, or interrupted |
BLOCKED | entering a synchronized block that's taken | the lock is released |
TERMINATED | run() returns or throws | never |
Exam tip: Java has no RUNNING state; running and ready are both RUNNABLE. Many textbooks draw a “Running” box anyway, so in exams you may write “Runnable (ready) ⇄ Running” and mention that getState() reports both as RUNNABLE.
The waiting states, side by side
TIMED_WAITING
sleep(ms), join(ms), wait(ms). Comes back by itself when the time is up.
WAITING
join(), wait(). Needs another thread to finish or call notify.
BLOCKED
Waiting to get a lock another thread holds. Only synchronized causes it.
RUNNABLE
Running, or ready and waiting for a CPU core. Busy loops stay here and burn CPU.
in the schedulerAll six states in one program
public class Main {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Thread sleeper = new Thread(() -> {
try { Thread.sleep(60_000); } catch (InterruptedException e) { }
}, "sleeper");
Thread waiter = new Thread(() -> {
try { sleeper.join(); } catch (InterruptedException e) { }
}, "waiter");
Thread blocked = new Thread(() -> {
synchronized (lock) { }
}, "blocked");
System.out.println("sleeper: " + sleeper.getState());
sleeper.start();
waiter.start();
synchronized (lock) { // main holds the lock...
blocked.start(); // ...so this thread can't get in
while (blocked.getState() != Thread.State.BLOCKED
|| sleeper.getState() != Thread.State.TIMED_WAITING
|| waiter.getState() != Thread.State.WAITING) {
Thread.onSpinWait(); // wait until all three have settled
}
System.out.println("sleeper: " + sleeper.getState());
System.out.println("waiter: " + waiter.getState());
System.out.println("blocked: " + blocked.getState());
}
sleeper.interrupt();
sleeper.join();
waiter.join();
blocked.join();
System.out.println("sleeper: " + sleeper.getState());
}
}The loop with Thread.onSpinWait() keeps main checking until all three helpers have reached their waiting states, so the output is the same every run. In real code you'd rarely wait like this; here it lets us take a clean snapshot.
The methods that move a thread
Thread.sleep(500); // pause the CURRENT thread 500 ms -> TIMED_WAITING
t.join(); // wait until t ends -> WAITING
t.join(200); // ...but at most 200 ms -> TIMED_WAITING
t.interrupt(); // politely ask t to stop
Thread.currentThread().isInterrupted(); // check the flag
t.getState(); t.isAlive(); // inspect
Thread.yield(); // hint: let another thread run (rarely needed)t.sleep(100) does not put t to sleep. sleep is static, so it always pauses the thread that runs the line. The compiler only warns. Always write Thread.sleep(…).
How interrupt() cancels a sleeping thread
The worker sleeps
The worker is TIMED_WAITING. The OS won't schedule it until the minute is up.try { Thread.sleep(60_000); } catch (InterruptedException e) { return; // stop cleanly }Another thread calls interrupt()
worker.interrupt()sets a boolean interrupted flag inside the worker's Thread object and wakes it if it's sleeping, waiting or joining.sleep throws
The worker becomes RUNNABLE, andsleepthrowsInterruptedException(message "sleep interrupted"). Throwing also clears the flag.The catch block decides
Clean up andreturn, sorun()ends and the thread becomes TERMINATED. If you can't stop here, callThread.currentThread().interrupt()to restore the flag for code further up.Busy threads must check
A thread that never sleeps or waits won't throw. Long loops should testThread.currentThread().isInterrupted()and stop when it's true.
Six states: both views
Left: the line a thread is on and the state getState() would report. Right: the state diagram with the transition taken.
new Thread(task)gives a thread in state NEW: an object, not yet running.- After
start()it is RUNNABLE: either running on a core, or ready and waiting for the scheduler to give it one. Java uses one state for both. Thread.sleep(500)puts it in TIMED_WAITING: it uses no CPU and wakes up by itself after the time is up (or when interrupted).- If it reaches a
synchronizedblock whose lock another thread holds, it is BLOCKED until the lock is free (Lesson 12.4). join()orwait()without a time limit puts it in WAITING: it needs another thread to finish or to notify it (Lesson 12.5).- When
run()returns or throws, the thread is TERMINATED. It can never run again.
- The six values of the enum
Thread.State. A brand-new Thread is NEW. start()is the only way out of NEW. RUNNABLE means the OS may run it whenever a core is free.- Timed waits (
sleep,join(ms),wait(ms)) go to TIMED_WAITING and come back on their own. - Trying to enter a
synchronizedblock held by another thread: BLOCKED. It returns when the lock is released. - Untimed waits (
join(),wait()) go to WAITING and need another thread to act. - From RUNNABLE, a finished
run()leads to TERMINATED. There is no arrow back: callingstart()again throwsIllegalThreadStateException.
Lifecycle in real systems
Knowing the states is how engineers read thread dumps: a snapshot of every thread and its state, taken when a server is slow or stuck.
Watch it with an example: a payment-status poller the user can cancel
After a UPI payment, the app checks the bank every 2 seconds until the status is final. If the user taps Cancel, main interrupts the poller.
- main creates the poller thread: NEW.
- After
start()it is RUNNABLE and calls the bank: still PENDING. Thread.sleep(2000): TIMED_WAITING. The thread uses no CPU while sleeping.- Time's up: back to RUNNABLE, and it asks again.
- The user taps Cancel. main calls
poller.interrupt(), which sets the poller's interrupted flag. - A sleeping thread that is interrupted wakes at once with
InterruptedException. The catch block stops the loop,run()returns, and the poller is TERMINATED. Interrupt is a polite request, not a kill switch.
1. Polling and retries
Payment-status checks, retrying a failed SMS after 5 seconds, refreshing a cache every minute: all use timed waits.
2. Cancel buttons
Cancel upload, stop export, abort search: the UI thread interrupts the worker, which stops at its next sleep, wait or flag check.
3. Thread dumps in production
jstack or monitoring tools list every thread with its state. Hundreds of BLOCKED threads on one lock point to a bottleneck; WAITING forever points to a missed notify or a deadlock.
4. Timeouts
Never wait forever for an outside system. join(ms) (and later Future.get(timeout)) lets the app give up and show a fallback.
Examples are simplified. Production code usually cancels tasks through executors and futures (Lesson 12.6), which use interrupts underneath.
A download that sleeps between updates
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread download = new Thread(() -> {
for (int pct = 25; pct <= 100; pct += 25) {
System.out.println("download " + pct + "%");
try {
Thread.sleep(40);
} catch (InterruptedException e) {
System.out.println("download cancelled");
return;
}
}
}, "download");
System.out.println("1. " + download.getState());
download.start();
download.join();
System.out.println("2. " + download.getState() + ", alive " + download.isAlive());
}
}download.getState()beforestart()Always NEW.Thread.sleep(40)Pauses the download thread (not main) between updates: TIMED_WAITING.catch (InterruptedException e)Required: sleep's checked exception can't escaperun(). The catch is where cancelling happens.download.join()main is WAITING until the download ends.
NEW, interrupted, TERMINATED
Recorded from the real JVM, following main. Watch the poller's state on the heap, and see how an interrupt turns a one-minute sleep into an instant stop.
Worked examples: real problems, solved and traced in memory
Three lifecycle tools you'll use constantly: sleep for pauses, interrupt for cancelling, and join(ms) for waiting with a time limit. The traces follow main and show the worker's state on the heap.
Order status ticks with sleep()
Problem statement
A delivery tracker moves an order through three statuses, pausing between updates. Run the updates on a tracker thread with Thread.sleep(50) between them. Main waits and then confirms the tracker has ended.
statuses: PLACED, COOKING, OUT FOR DELIVERY
Order #88: PLACED Order #88: COOKING Order #88: OUT FOR DELIVERY tracker TERMINATED, alive false
Approach
- The tracker loops over the statuses, printing each and sleeping in between.
sleepthrows a checked exception, so catch it inside the lambda.- main joins, then checks
getState()andisAlive().
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Cancel a slow report with interrupt()
Problem statement
A report job processes rows, pausing after each one (simulating slow disk reads). The user cancels right away. The job must notice the interrupt, stop cleanly, and say how many rows it finished.
rows 1..5, cancel immediately
cancelled after 1 row(s) report state: TERMINATED
Approach
- The worker processes a row, then sleeps 1 second.
- main calls
interrupt()straight afterstart(). - The sleep throws
InterruptedException; the catch reports progress and returns.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Wait at most 100 ms for a slow bank API
Problem statement
A balance check calls a bank API that sometimes hangs. main waits for it with join(100). If it hasn't finished by then, main reports a timeout, cancels the call with interrupt(), and shows a cached balance.
cached balance Rs 12,400; the API hangs
timeout: bank-api is TIMED_WAITING api call cancelled showing cached balance Rs 12,400
Approach
- The API thread sleeps 60 s to simulate a hung call.
join(100)returns after at most 100 ms, finished or not.- Check
isAlive(): if still alive, interrupt it and use the cached value.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Check yourself
Quick revision
Lesson 12.3 in one screen
- Six states: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED (
Thread.State). - RUNNABLE = running or ready; Java has no separate RUNNING state.
sleep(ms),join(ms)→ TIMED_WAITING;join(),wait()→ WAITING; busy lock → BLOCKED.Thread.sleepalways pauses the current thread.interrupt()sets a flag; sleep/join/wait then throwInterruptedExceptionand clear the flag.- Use
join(ms)+isAlive()for timeouts. - TERMINATED is final; a thread can't be restarted.
Interview and viva questions
Explain the lifecycle of a thread in Java.
A thread is NEW after construction. start() makes it RUNNABLE (running or ready). It may move to TIMED_WAITING (sleep, timed join/wait), WAITING (join, wait) or BLOCKED (waiting for a monitor lock), and returns to RUNNABLE when the condition ends. When run() completes or throws, it is TERMINATED.
What is the difference between WAITING and BLOCKED?
BLOCKED means the thread wants to enter a synchronized block/method and another thread holds the lock. WAITING means the thread chose to wait (join, wait) until another thread does something. A thread in wait() also releases the lock; a BLOCKED thread holds nothing.
What is the difference between sleep() and wait()?
sleep is a static Thread method that pauses the current thread for a time and keeps any locks it holds. wait is an Object method that must be called while holding that object's lock; it releases the lock and waits for notify or a timeout (Lesson 12.5).
How do you stop a thread in Java?
Cooperatively: call interrupt(), and write the thread so it stops when sleep/wait throws InterruptedException or when isInterrupted() is true. Thread.stop() was deprecated because killing a thread mid-operation can leave shared data corrupted.
What happens to the interrupted flag when InterruptedException is thrown?
It is cleared. If your code catches the exception but can't stop right there, it should call Thread.currentThread().interrupt() to restore the flag so code higher up can see the request.
Practice problems
Observe states, pause with sleep, wait with join, and cancel with interrupt. Every output is fixed. The last three are business tasks.
State before and after
Print a thread's state before start() and after join().
(none)
before: NEW after: TERMINATED
Show solution and explanation
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {});
System.out.println("before: " + t.getState());
t.start();
t.join();
System.out.println("after: " + t.getState());
}
}How it works: NEW until start(); after join() returns the thread is certainly TERMINATED. Any state in between depends on timing.
isAlive()
Print isAlive() before starting a thread and after it has finished.
(none)
alive before start: false working alive after join: false
Show solution and explanation
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> System.out.println("working"));
System.out.println("alive before start: " + t.isAlive());
t.start();
t.join();
System.out.println("alive after join: " + t.isAlive());
}
}How it works: a thread is alive from start() until it terminates. NEW and TERMINATED threads are not alive.
Countdown with sleep
Print 3, 2, 1, Go! with a 100 ms pause between numbers, on the main thread.
(none)
3 2 1 Go!
Show solution and explanation
public class Main {
public static void main(String[] args) throws InterruptedException {
for (int i = 3; i >= 1; i--) {
System.out.println(i);
Thread.sleep(100);
}
System.out.println("Go!");
}
}How it works: Thread.sleep is static: it always pauses the current thread (here, main). main declares throws InterruptedException.
Interrupt a sleeping thread
Start a thread that sleeps 10 seconds. Interrupt it at once and print the exception message it catches.
(none)
caught: sleep interrupted
Show solution and explanation
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
System.out.println("caught: " + e.getMessage());
}
});
t.start();
t.interrupt();
t.join();
}
}How it works: interrupt() makes sleep throw InterruptedException (message "sleep interrupted"), so the program ends in milliseconds, not 10 seconds.
The flag is cleared
Interrupt the current thread, then call sleep. Print the interrupted flag after catching the exception.
(none)
flag set: true sleep threw at once flag now: false
Show solution and explanation
public class Main {
public static void main(String[] args) {
Thread.currentThread().interrupt();
System.out.println("flag set: " + Thread.currentThread().isInterrupted());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("sleep threw at once");
}
System.out.println("flag now: " + Thread.currentThread().isInterrupted());
}
}How it works: when sleep throws InterruptedException it also clears the flag. If you can't handle the interrupt, restore it with Thread.currentThread().interrupt() so callers still see it.
Join in a chain
Thread B must print only after thread A has finished. Make B call a.join() before printing. main joins B.
(none)
A: data downloaded B: data processed main: all done
Show solution and explanation
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(() -> System.out.println("A: data downloaded"));
Thread b = new Thread(() -> {
try {
a.join();
} catch (InterruptedException e) {
return;
}
System.out.println("B: data processed");
});
a.start();
b.start();
b.join();
System.out.println("main: all done");
}
}How it works: any thread can join any other. B is WAITING inside a.join() until A terminates, so B's line always comes second, whichever thread the scheduler runs first. (Careful: if B called a.join() before A was started, join would return at once, because a NEW thread isn't alive.)
OTP expiry timer
An OTP is valid for 3 ticks. A timer thread prints the remaining ticks with 50 ms sleeps and then marks the OTP expired. main waits and prints the final status.
OTP 482913, 3 ticks
OTP 482913 valid for 3 more tick(s) OTP 482913 valid for 2 more tick(s) OTP 482913 valid for 1 more tick(s) OTP status: EXPIRED
Show solution and explanation
public class Main {
static String status = "VALID";
public static void main(String[] args) throws InterruptedException {
Thread timer = new Thread(() -> {
try {
for (int left = 3; left > 0; left--) {
System.out.println("OTP 482913 valid for " + left + " more tick(s)");
Thread.sleep(50);
}
status = "EXPIRED";
} catch (InterruptedException e) {
status = "CANCELLED";
}
}, "otp-timer");
timer.start();
timer.join();
System.out.println("OTP status: " + status);
}
}How it works: the timer is mostly TIMED_WAITING. If the user verified the OTP early, main could interrupt() the timer and the status would become CANCELLED.
Cancel a bulk SMS run
A marketing job sends 1,000 SMS, one per 500 ms. Operations cancels it right after start. The job must check for the interrupt, stop, and report how many were sent.
1,000 messages, cancelled immediately
campaign stopped by operator messages sent: 1 of 1000
Show solution and explanation
public class Main {
public static void main(String[] args) throws InterruptedException {
int[] sent = {0};
Thread job = new Thread(() -> {
for (int i = 1; i <= 1000; i++) {
sent[0]++;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("campaign stopped by operator");
return;
}
}
}, "sms-campaign");
job.start();
job.interrupt();
job.join();
System.out.println("messages sent: " + sent[0] + " of 1000");
}
}How it works: the job checks for cancellation at every sleep. A thread that never sleeps or checks isInterrupted() can't be cancelled politely; that's why long loops should check the flag.
Pipeline with join timeouts
A nightly pipeline has a fast step (sleeps 10 ms) and a stuck step (sleeps 60 s). Give each join(200). Print which steps completed and cancel the stuck one.
steps: export (fast), upload (stuck)
export: OK upload: TIMEOUT upload cancelled
Show solution and explanation
public class Main {
static Thread step(String name, long ms) {
return new Thread(() -> {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
System.out.println(name + " cancelled");
}
}, name);
}
public static void main(String[] args) throws InterruptedException {
Thread[] steps = {step("export", 10), step("upload", 60_000)};
for (Thread s : steps) s.start();
for (Thread s : steps) {
s.join(200);
if (s.isAlive()) {
System.out.println(s.getName() + ": TIMEOUT");
s.interrupt();
s.join();
} else {
System.out.println(s.getName() + ": OK");
}
}
}
}How it works: join(ms) never blocks longer than the limit. Combining it with isAlive() and interrupt() is the classic timeout pattern for raw threads (executors give you Future.get(timeout), Lesson 12.6).
Real-world scenario questions (asked in company interviews)
These test whether you can reason about states in a running system, not just recite them.
150 threads BLOCKED on one line
During a sale, the order service becomes slow. A thread dump shows 150 request threads BLOCKED waiting for the same lock in InventoryService.reserve(), and one thread RUNNABLE inside it, calling a slow pricing API. What's going on?
The interviewer is checking: reading states to find a bottleneck.
Show a strong answer
- The method is
synchronized, and the thread holding the lock is doing slow I/O inside it, so every other request queues up as BLOCKED. - Move the pricing call outside the locked section, and lock only the few lines that update shared stock, or use finer-grained locks.
Avoid: adding more threads; they would just join the BLOCKED queue.
The Cancel button that doesn't cancel
An export thread's code is try { Thread.sleep(100); } catch (InterruptedException e) { } inside a loop of 10,000 rows. Pressing Cancel calls interrupt(), but the export keeps going. Why?
The interviewer is checking: swallowed interrupts.
Show a strong answer
- The empty catch swallows the interrupt, and throwing cleared the flag, so the loop just continues.
- In the catch, stop the loop (
returnorbreak), or restore the flag withThread.currentThread().interrupt()and check it in the loop condition.
Avoid: “use Thread.stop()”. It's deprecated and unsafe.
A partner API that sometimes never answers
A travel site's search calls three airline APIs on three threads and then calls join() on each. Once a week one airline's API hangs and the search page never loads. Fix it.
The interviewer is checking: timed waits and fallbacks.
Show a strong answer
- Plain
join()waits forever. Use a deadline:join(remainingMs)per thread (orFuture.get(timeout)). - After the deadline, interrupt the slow thread, show results from the airlines that answered, and log the timeout.
- Also set network timeouts in the HTTP client itself.
Avoid: waiting forever for anything outside your system.
A “waiting” thread using 100% CPU
A worker waits for a flag with while (!ready) { }. The server shows one core at 100%, and the thread dump says the worker is RUNNABLE. Explain and fix.
The interviewer is checking: busy-waiting vs blocking states.
Show a strong answer
- A busy loop never leaves RUNNABLE, so it burns a whole core doing nothing useful. It may also never see the change if
readyisn'tvolatile(Lesson 12.4). - Use a blocking mechanism:
wait/notify(Lesson 12.5), aCountDownLatch, aBlockingQueue, orjoin(), so the thread is WAITING and uses no CPU.
Avoid: adding Thread.sleep(1) in the loop as the final fix; it still wakes up constantly and adds delay.