Java Threads & Concurrency API — Part I
The classic thread model is heavier than it looks — which is exactly why the JVM eventually stopped mapping threads one-to-one to the OS.

A thread is the unit of independent execution — a path through your code that can run alongside other paths. In Java the classic way to make one is to hand a Runnable, a chunk of work, to a Thread and start it. That much every tutorial covers. What most of them skip is the price. Every one of these traditional threads — now called platform threads — is a thin wrapper over an operating-system thread, mapped one to one. And that mapping isn't free: each one reserves on the order of a megabyte of stack, and the OS has to schedule it. Create a few hundred and you're fine. Create tens of thousands and you run out of memory or grind the scheduler to a halt. That single fact — threads are expensive because they are OS threads — shapes almost every concurrency decision that follows.
Runnable, Thread, and the lifecycle
There are two ways to define the work: implement Runnable and pass it to a Thread, or subclass Thread directly. Prefer Runnable. It separates the task from the machinery that runs it, and it doesn't spend your one shot at inheritance on plumbing. Once started, a thread moves through a lifecycle — new, runnable, running, blocked or waiting, terminated — and the transitions worth caring about are the ones where it stops running: waiting on a lock, sleeping, blocked on I/O. Which brings up the expensive part.
The real problem: blocking wastes the thing you paid for
Here is the waste that motivated everything that came after. When a platform thread blocks — waiting on a database, a network call, a file read — the OS thread underneath it just sits there, parked, still holding its megabyte of stack, doing nothing until the call returns. You paid for an OS thread and it's idle. For a server handling thousands of simultaneous connections, each of them mostly waiting on I/O, this is brutal: you'd like a thread per connection, but threads are the scarce resource. Much of the history of Java concurrency — thread pools, executors, async callbacks — is a series of workarounds for that one problem.
Why pools exist
Because threads are costly to create and limited in number, you don't spin up a fresh one per task. You keep a pool of them and hand work to it through an ExecutorService, which reuses a fixed set of threads across many tasks. That's the pragmatic answer for platform threads: stop making new ones, recycle a bounded set. It works, at the cost of code that reads worse — you stop writing simple "do this, then wait, then do that" and start juggling futures and callbacks so you don't tie up a precious thread while it waits.
Where this is heading
It's worth naming the plot twist, because it recontextualises everything above. Modern Java, from version 21, added virtual threads — threads managed by the JVM instead of the OS, cheap enough that you can have millions, and which automatically step aside when they block instead of holding an OS thread hostage. They don't replace understanding platform threads; they're built on the same Runnable and Thread API you're learning here. But they dissolve the specific problem this whole part is about — the one where blocking wastes an expensive resource.
So learn the classic model first. It's still the foundation and the shared vocabulary. Just know, as you learn it, that the "threads are scarce, don't waste them" assumption sitting underneath all of it is the exact thing the platform eventually engineered its way around.






