The idea
What it is
A thread is a single, independent path of execution inside a program — one worker that runs code line by line. Every program starts with exactly one: the main thread. Creating more threads means more work can be in flight at the same time. Here's the one-line difference from a process: a process is a whole running program with its own private memory, while threads live inside one process and share its memory — same variables, same objects, same heap. That sharing is what makes threads fast to create and talk to, and it's also the reason every hard concurrency bug exists.
Picture a restaurant kitchen. The kitchen is your program. Each cook is a thread, and every cook works from the same shared fridge and the same shared counter — that's shared memory. The stoves are CPU cores: no matter how many cooks you hire, only as many can actually be cooking as you have stoves. Hiring a cook is not the same as putting them on shift; being on shift is not the same as standing at a stove. A cook can be stuck waiting for the oven to free up, or taking a timed five-minute break, or finished for the day. Those situations are exactly the lifecycle states of a thread.
And here's the part beginners always trip on: you don't decide who cooks when. The operating system's scheduler does. It hands a core to one ready thread, takes it away a few milliseconds later, gives it to another — on its own schedule, for its own reasons. Your code says what each thread does; the scheduler says when. So any program whose correctness depends on "thread A will surely get there before thread B" is already broken; it just hasn't failed yet.
The one sentence to remember
start() creates a new worker that runs run() somewhere else; calling run() yourself is just an ordinary method call on the thread you're already on — no new worker, no concurrency at all.
You're already surrounded by threads
Your web server handles each incoming request on a thread. Your phone app keeps a UI thread that must never block, which is why slow work is pushed to a background thread. A garbage collector, a logger flushing to disk, a timer firing callbacks — all threads. You have been writing multi-threaded programs for a while; you just weren't the one starting the threads.
Mechanics
How it works
The lifecycle: five states a thread moves through
A thread's whole life is a small state machine. Java names these states in the Thread.State enum, and the same shapes exist in every language:
- NEW — the
Threadobject exists but nothing is running yet. The cook is hired, not on shift. No operating-system thread has been created at this point. - RUNNABLE — started and eligible to run: it is either on a core right now or standing in the queue waiting for one. This is the state beginners misread. RUNNABLE means ready, not necessarily executing. Ten RUNNABLE threads on a four-core machine means at most four are truly moving.
- RUNNING — actually executing instructions on a core. (Java folds this into RUNNABLE, but it helps to picture it separately: cooks at a stove.) The count of RUNNING threads can never exceed the number of cores.
- BLOCKED / WAITING / TIMED_WAITING — alive but parked, using no CPU. BLOCKED = waiting to acquire a lock someone else holds. WAITING = parked indefinitely until another thread signals it (
join(),wait()). TIMED_WAITING = parked with a deadline (Thread.sleep(1000),join(500),await(2, SECONDS)). A parked thread hands its core back — that's why blocking isn't "burning CPU". - TERMINATED —
run()returned (or threw). The shift is over, permanently. A terminated thread cannot be restarted, revived, or reused.
start() vs run() — the classic first mistake
Thread has a method called run() containing the work, and a method called start() that launches it. They look interchangeable and are absolutely not. start() asks the OS for a real new thread, which then calls run() over there. Calling run() yourself is a plain method call: the code executes on your current thread, top to bottom, before the next line runs. Nothing is concurrent, and no new thread was ever born — the biggest silent bug a beginner can ship.
Thread cook = new Thread(() -> System.out.println("cooking on " +
Thread.currentThread().getName())); // state: NEW
cook.run(); // prints "cooking on main" ← just a method call, 1 lane
cook.start(); // prints "cooking on Thread-0" ← a REAL second worker
cook.start(); // 💥 IllegalThreadStateException — start() is one-shot
cook.join(); // the CALLING thread waits here until 'cook' is TERMINATEDOrdering is never guaranteed
Start Cook-1 then Cook-2 and you might see Cook-2's output first — every run can differ, and the version that "works on my machine" may fail on a busier or bigger machine. The scheduler is free to interleave threads at any instruction boundary. If you need ordering, you must create it explicitly with join(), a latch, a queue, or a lock. Never with sleep() and hope.
A Thread object is one-shot
Calling start() twice on the same Thread object throws IllegalThreadStateException — in Python it's a RuntimeError, in C++ starting a std::thread twice isn't even expressible. The reason is simple: start() is the transition out of NEW, and a thread that has left NEW can never return to it. If you want to do the work again, create a new Thread object (or, far better, hand the task to a pool). The task and the worker are different things: a Runnable is reusable, a Thread is not.
Waiting on purpose: join() and sleep()
t.join()— "I'll wait here until threadtfinishes." Note who waits: the calling thread parks in WAITING, nott. This is the standard waymainavoids exiting while its workers are still plating dishes, and the standard way to collect results before moving on.Thread.sleep(ms)— parks the current thread in TIMED_WAITING for at least that long, then it becomes RUNNABLE again. Two catches: at least is not exactly (it must still wait for a core after waking), and sleeping is not a synchronization tool — it does not release any lock it holds and it does not make a race condition go away.- Interruption —
t.interrupt()is a polite request, not a kill. It makes a sleeping or waiting thread throwInterruptedException; a thread busy computing simply sees a flag. There is no safe way to force-stop a thread, which is whyThread.stop()was deprecated decades ago.
Daemon vs user threads, in one line
A user thread keeps the program alive: the JVM exits only when the last one finishes. A daemon thread (t.setDaemon(true), before start()) is background help — a metrics flusher, a cache cleaner — and the program will exit right out from under it without waiting. Rule of thumb: if losing the work mid-way would be a bug, it must be a user thread.
Why you rarely write new Thread(...) today
Raw threads are expensive: each one costs the OS a stack (often ~1 MB) and a scheduling slot, creation takes real time, and a request-per-thread design collapses under load — thousands of cooks in a kitchen with eight stoves spend all their time bumping into each other (that's context switching). So in production you almost never hire a cook per dish. You keep a small permanent crew and feed them a queue of tasks: an ExecutorService in Java, a ThreadPoolExecutor in Python, a worker pool in C++. Threads become reused workers instead of one-shot objects — which is exactly the next lesson, Thread pools.
Threads aren't always OS threads
Go's goroutines and Java 21's virtual threads are lightweight threads managed by the runtime, multiplexed onto a few real OS threads. They cost kilobytes instead of megabytes, so a million of them is fine. The lifecycle picture you just learned still applies — created, runnable, running, parked, done — only the price tag changed.
Interactive prototype
See it. Build it. Break it.
A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.
About this simulation
A kitchen with two cooks and one stove. Pick a cook card (👩🍳 Cook-1 / 🧑🍳 Cook-2) — the buttons drive whoever is selected — then hit ▶ start(): the lifecycle track lights up NEW → RUNNABLE → RUNNING, the 🔥 core ×1 slot fills with that cook, and the caption reads threads alive: 1 · on a core: 1. Start the second cook and watch it park in the ready queue as RUNNABLE — ready, but there is only one stove. Press ⇄ Scheduler tick to see the core change hands (you didn't choose who). ⏳ sleep(1s) parks a cook in TIMED_WAITING and frees the stove; 🔒 wait for lock parks it in BLOCKED until ✅ release; 🏁 finish ends it at TERMINATED. Press ▶ start() on an already-started cook and the node flashes red: IllegalThreadStateException. Finally flip the call chip from start() to run() — now the button creates no worker at all: the core shows 🧵 main doing the work and threads alive stays 0.
Hands-on
Try these yourself
Open the prototype above, predict what happens, then verify.
Hire a cook and put them on shift
With the call chip on start(), make sure 👩🍳 Cook-1 is selected and press ▶ start(). The track lights NEW, then RUNNABLE, then RUNNING; the 🔥 core ×1 slot fills with 'Cook-1 · RUNNING' and the caption reads threads alive: 1 · on a core: 1. Now press ▶ start() again on the same cook: the node flashes red with 'start() twice ✗ IllegalThreadStateException'. One Thread object, one start — ever.
Two cooks, one stove
Click the 🧑🍳 Cook-2 card and press ▶ start(). It does not begin cooking — its chip reads RUNNABLE and it appears in the ready queue, because the single core is taken. That is the whole meaning of RUNNABLE: ready, not running. Press ⇄ Scheduler tick a few times and watch the core change hands; you never chose who. Then select the cook holding the core and try ⏳ sleep(1s) (TIMED_WAITING, core handed over, wakes on its own), 🔒 wait for lock followed by ✅ release (note it returns to the queue, not straight to the stove), and 🏁 finish for TERMINATED.
The run() trap
Hit ↺ Reset, then flip the call chip from start() to run() and press ▶ run(). Nothing enters the lifecycle: Cook-1's chip stays NEW, threads alive stays 0, and the core slot shows '🧵 main · doing Cook-1's work'. One lane, zero concurrency — run() is just a method call. Flip back to start() and press it again to see a real second lane appear.
In practice
When to use it — and what trips people up
Start a thread when...
- Something slow must not freeze everything else — a network call, a file read, a long computation. Move it off the UI or request thread so the rest of the program stays responsive.
- Independent work can overlap — several downloads, several files to parse. If the pieces don't depend on each other, more workers means less wall-clock time.
- You need real parallel CPU work — genuinely splitting a big computation across cores. Note: on multi-core machines this is real speed-up; in CPython the GIL means CPU-bound work needs processes, not threads.
Don't hand-roll threads when...
- There will be many short tasks — creating a thread per task is pure overhead. Use a pool (
ExecutorService,ThreadPoolExecutor) and reuse a small crew. - The work is trivially fast — starting a thread costs more than the work saves. Just call the method.
- You'd use
sleep()to get the ordering right — that's a race condition with a timer taped over it. Usejoin(), a latch, a queue, or a lock.
What it gives you
- Responsiveness — slow work moves off the main or UI thread, so the program keeps reacting instead of freezing.
- Real parallelism — independent work spreads across cores, cutting wall-clock time on multi-core machines.
- Cheap sharing — threads live in one process and share memory, so passing data between them needs no copying or serialization.
- Natural fit for blocking I/O — a thread parked on a socket costs no CPU while it waits, and wakes up right when the data arrives.
Common mistakes
- Shared memory means shared bugs — races, torn reads, deadlocks and visibility problems all come from the same sharing that makes threads fast.
- Non-determinism — the scheduler interleaves threads differently every run, so bugs appear rarely and vanish under a debugger.
- Expensive at scale — each OS thread costs a stack (~1 MB) plus scheduling; thousands of them thrash the CPU with context switches.
- One-shot and unmanaged — a raw Thread can't be restarted, cancelled safely, rate-limited or reused, which is why pools exist.
Reference
Code & further reading
A minimal reference implementation and pointers worth bookmarking.
public class Kitchen {
// The TASK (a Runnable) is not the WORKER (a Thread). Tasks are reusable.
static Runnable cook(String name, int dishes) {
return () -> {
for (int i = 1; i <= dishes; i++) {
System.out.println(name + " plated dish " + i);
try {
Thread.sleep(100); // TIMED_WAITING: gives the core back
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore the flag, then stop
return;
}
}
};
}
public static void main(String[] args) throws InterruptedException {
Thread c1 = new Thread(cook("Cook-1", 3), "Cook-1"); // state: NEW
Thread c2 = new Thread(cook("Cook-2", 3), "Cook-2"); // state: NEW
// c2.setDaemon(true); // background help: would NOT keep the JVM alive
c1.start(); // NEW -> RUNNABLE: a real second worker exists
c2.start();
// c1.run(); // would run the task on MAIN — no new thread at all
// c1.start(); // IllegalThreadStateException — start() is one-shot
c1.join(); // MAIN parks in WAITING until Cook-1 is TERMINATED
c2.join();
// Output order between the two cooks changes from run to run.
System.out.println("closed; Cook-1 is " + c1.getState()); // TERMINATED
}
}References & further reading
8 sources- Docsdocs.oracle.com
Thread (Java SE 21 API) — Oracle
The authoritative source: start(), run(), join(), sleep(), interrupt(), daemon threads, and the Thread.State enum listing every lifecycle state by name.
- Articlejenkov.com
Creating and Starting Java Threads — Jenkov
The clearest beginner walkthrough of start() vs run(), including the exact mistake of calling run() and wondering why nothing is concurrent.
- Articlebaeldung.com
Life Cycle of a Thread in Java — Baeldung
Each state (NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED) with a tiny runnable program that actually prints the thread in that state.
- Docsdocs.python.org
threading — Thread-based parallelism (Python docs)
Python's side of the same model: start(), join(), daemon threads, and the explicit note that start() may be called at most once per thread object.
- Docsen.cppreference.com
std::thread — cppreference
C++ threads start on construction, and any joinable thread must be joined or detached before destruction — the one lifecycle rule C++ enforces with std::terminate.
- Docsgo.dev
Effective Go — Goroutines
How a runtime-managed lightweight thread differs from an OS thread, and why 'go f()' versus 'f()' is exactly the start() versus run() distinction.
- Talkgo.dev
Concurrency is not Parallelism — Rob Pike (talk)
A 30-minute video that fixes the single most common confusion in this topic: structuring work as independent threads is not the same thing as running it simultaneously.
- Bookjcip.net
Java Concurrency in Practice — Goetz et al.
The standard book. Chapters on thread creation, safe cancellation and interruption explain why raw threads give way to executors in real systems.
Knowledge check
Did it land?
Quick questions, answers revealed on submit. Sign in to save your best score.
question 01 / 06
What is the difference between calling t.start() and calling t.run()?
question 02 / 06
A thread's state is RUNNABLE. What does that actually tell you?
question 03 / 06
You call t.start(), the thread finishes its work, and later you call t.start() again on the same object. What happens?
question 04 / 06
In main, you write worker.join(). Which thread waits, and until when?
question 05 / 06
You start Cook-1 and then Cook-2, and each prints its name. Is Cook-1's line guaranteed to print first?
question 06 / 06
What is the key difference between a process and a thread?
0/6 answered