Thread & Goroutine Lifecycle
A Java thread moves through NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED. A goroutine has no public state, it runs until its function returns; the runtime parks/unparks it on channel and lock operations. Python threads expose start/join/is_alive but no fine-grained state.
Diagram
What it is
Every concurrent unit (Java thread, Python thread, Go goroutine, asyncio task) goes through a small lifecycle: it gets created, runs for a while (sometimes parked or blocked), and eventually finishes. The languages differ in how much of that state they expose and in the mechanism for signalling "please stop."
Java's thread state machine
Java is the language with the most explicit state machine. Six states, all observable via Thread.getState(). The diagram above shows every transition:
- NEW: created with
new Thread(...), not yet started. - RUNNABLE: started; the OS may further split this into "running on a CPU" vs "ready to run", but the JVM doesn't expose that.
- BLOCKED: trying to enter a
synchronizedblock whose monitor is held. - WAITING: parked indefinitely via
wait(),join(), orpark()with no timeout. - TIMED_WAITING: parked with a timeout via
sleep(ms),wait(ms),join(ms), orparkNanos. - TERMINATED:
run()returned.
These states are for diagnostics (thread dumps, monitoring), not for control. Avoid writing if (t.getState() == BLOCKED) .... Coordinate with locks, latches, futures, channels instead.
Goroutines and Python: less visible
Goroutines have no public state machine. The Go runtime parks a goroutine when it's blocked on a channel, a lock, or a syscall, and resumes it when the wait is satisfied. From a caller's view, the only question is "is this goroutine still running?" usually answered by a WaitGroup, a closed channel, or a context being done.
Python threads are even simpler: start(), join(), is_alive(). No state enum. No interrupt mechanism. Cancellation is a convention: use threading.Event and have the worker poll it. asyncio adds Task.cancel() which raises CancelledError at the next await.
Why this matters
Lifecycle bugs are the worst kind of production outage: nothing looks broken, metrics are fine, but the process slowly accumulates leaked threads/goroutines until it OOMs at 4 a.m. Interviewers ask about this because graceful shutdown, cancellation, and leak prevention are the three skills that separate competent concurrent code from production-grade.
Cancellation patterns
Three cancellation models
- Java:
Thread.interrupt()sets a flag and unblockssleep/wait/joinwithInterruptedException. Cooperative, the thread must check. - Go:
context.Contextcarries aDone()channel and an optional deadline. Cooperative, the goroutine mustselecton it. - Python:
threading.Eventfor threads,Task.cancel()for asyncio. Threads are polling-based; asyncio tasks receive aCancelledErrorat the nextawait.
All three are cooperative for the same reason: forcibly killing a thread mid-operation can leave shared state in an inconsistent place. The cancellation primitive requests shutdown; the worker decides when it's safe to actually stop.
When to reach for what
- Long-running server task that should stop on request:
context.Context(Go), interrupt + check (Java),Event+ poll (Python). - Time-bounded operation:
context.WithTimeout,Future.get(timeout),asyncio.wait_for. - Wait for many workers to finish:
WaitGroup(Go),ExecutorService.awaitTermination(Java),asyncio.gather(Python). - Force-kill a misbehaving worker: there is no clean way. Restart the process.
The shutdown checklist
Before shipping a long-running concurrent service, answer: how does every worker exit on SIGTERM? Without a precise answer for each one, a leak is waiting to happen.
Primitives by language
- Thread.State enum
- Thread.start() / join() / interrupt()
- Object.wait() / notify() / notifyAll()
- LockSupport.park() / unpark()
- ExecutorService shutdown / shutdownNow / awaitTermination
Implementations
getState() exposes the JVM's view of the thread. NEW before start, WAITING after wait(), TERMINATED after the run method returns. Useful for diagnostic dashboards but not for control flow.
1 Object lock = new Object();
2
3 Thread t = new Thread(() -> {
4 synchronized (lock) {
5 try { lock.wait(); }
6 catch (InterruptedException e) {
7 Thread.currentThread().interrupt();
8 }
9 }
10 });
11
12 System.out.println(t.getState()); // NEW
13 t.start();
14 Thread.sleep(50);
15 System.out.println(t.getState()); // WAITING
16
17 synchronized (lock) { lock.notify(); }
18 t.join();
19 System.out.println(t.getState()); // TERMINATEDinterrupt() sets a flag and unblocks sleep/wait/join with InterruptedException. The thread must check the flag (or catch the exception) and exit voluntarily, Thread.stop() is deprecated because it leaves data structures inconsistent.
1 Thread worker = new Thread(() -> {
2 while (!Thread.currentThread().isInterrupted()) {
3 try {
4 Thread.sleep(1000);
5 doWork();
6 } catch (InterruptedException e) {
7 Thread.currentThread().interrupt(); // re-set; sleep cleared it
8 break;
9 }
10 }
11 cleanup();
12 });
13 worker.start();
14
15 worker.interrupt(); // request cancellation
16 worker.join();Two-phase shutdown: shutdown() rejects new tasks but lets in-flight ones finish. If they don't finish in time, shutdownNow() interrupts them. Always pair with awaitTermination and a timeout, never assume tasks exit promptly.
1 ExecutorService pool = Executors.newFixedThreadPool(4);
2 pool.submit(() -> doWork());
3
4 pool.shutdown();
5 if (!pool.awaitTermination(10, TimeUnit.SECONDS)) {
6 pool.shutdownNow();
7 if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
8 System.err.println("Pool did not terminate");
9 }
10 }Key points
- •Java thread states: NEW, RUNNABLE, BLOCKED (waiting for monitor), WAITING, TIMED_WAITING, TERMINATED
- •RUNNABLE in Java covers both 'running' and 'ready to run', the JVM doesn't expose CPU-on/off
- •BLOCKED = waiting for a synchronized monitor; WAITING = parked via wait/join/park
- •Goroutines have no public state, they're either running, runnable, or parked
- •Cancellation in Go is via context.Context, propagated explicitly through call chains
- •Cancellation in Java is via Thread.interrupt(), checked cooperatively
- •Cancellation in Python is via threading.Event polling or asyncio.Task.cancel()
Tradeoffs
| Option | Pros | Cons | When to use |
|---|---|---|---|
| Thread.interrupt() (Java) |
|
| Cancelling Java threads, especially blocking I/O or sleep |
| context.Context (Go) |
|
| Every Go RPC handler, request scope, anywhere cancellation matters |
| threading.Event (Python) |
|
| Long-running Python worker threads |
| asyncio.Task.cancel() |
|
| asyncio-based services and pipelines |
Follow-up questions
▸What's the difference between BLOCKED and WAITING in Java?
▸Why is Thread.stop() deprecated?
▸How is a goroutine cancelled?
▸What happens to goroutines when main() exits?
▸Can a Thread be restarted after it terminates?
▸How does Python cancel a thread?
Gotchas
- !Thread.sleep() and Object.wait() CLEAR the interrupt flag, must re-set with Thread.currentThread().interrupt()
- !Forgetting wg.Add() before 'go' creates a race with wg.Wait(), Add must be called from the parent goroutine
- !context.WithCancel without calling cancel() leaks the context's goroutine
- !Daemon threads (Java setDaemon, Python daemon=True) are killed at shutdown, avoid for cleanup work
- !Calling notify() vs notifyAll(), notify wakes ONE waiter; spurious wakeups require waiting in a loop
- !Swallowing asyncio.CancelledError prevents shutdown, re-raise after cleanup
Common pitfalls
- Catching InterruptedException and ignoring it, cancellation signal is lost
- Spinning on Thread.State or runtime stats instead of using proper coordination primitives
- Calling .cancel() on a Future and assuming the work stopped, it might already be running
Practice problems
APIs worth memorising
- Java: Thread.{start, join, interrupt, isInterrupted, getState}; ExecutorService.{shutdown, shutdownNow, awaitTermination}
- Python: threading.Thread.{start, join, daemon}, threading.Event, asyncio.Task.cancel()
- Go: context.{Background, WithCancel, WithTimeout, WithDeadline}; sync.WaitGroup
Every long-running worker, server connection handler, scheduled job. Graceful shutdown depends on getting lifecycle right.