Skip to main content

Command Palette

Search for a command to run...

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.

Updated
4 min readView as Markdown
Java Threads & Concurrency API — Part I
I
Welcome to Bits8Byte! I’m Ish, an AI Engineer with 13+ years of experience across software engineering, automation, cloud, and AI-driven systems. This blog is where I share practical insights, technical deep dives, and real-world lessons from building modern software and exploring the fast-moving world of AI. My background spans Java, Spring Boot, Python, FastAPI, AWS, Docker, Kubernetes, DevOps, observability, and automation. Today, my work is increasingly focused on AI engineering, including LLM applications, AI agents, production-grade microservices, and scalable cloud-native architectures. Here, you’ll find thoughtful writing on AI trends, engineering best practices, software architecture, and the mindset required to adapt and grow in the age of AI. My aim is not just to explain technology, but to make it useful, practical, and grounded in real implementation experience. Thanks for stopping by. I hope this space helps you learn something valuable, think more deeply, and stay ahead in a rapidly evolving industry.

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.