Java Multithreading Introduction
What threads are, why you'd use them in Java, and the trade-offs of concurrent programming.
Every Java program you've written so far has had one thread of execution — one cursor stepping through the bytecode, one variable on the stack, one method call at a time. That's the "main" thread the JVM starts for you. Multithreading is the JVM running several such cursors at once, sharing the same heap. Two threads can be inside two different methods on two different objects at the same instant — and that's both the power and the danger.
Modern CPUs have many cores. A single-threaded program leaves all but one of them idle. A web server handling one request at a time can't use a 16-core machine any more than it could use a 1-core machine. The whole reason multithreading exists is to put those cores to work and to keep the program responsive when one part of it is waiting (for the disk, the network, the user).
What a thread actually is
A Java thread is two things glued together:
- An OS-level thread the operating system schedules onto a CPU core. It has a program counter, a register set, and a native stack. The OS time-slices it with every other runnable thread on the machine.
- A Java object of type
java.lang.Thread. It carries a name, a priority, a daemon flag, and — most importantly — a reference to theRunnablewhoserun()method it will execute.
When you call thread.start(), the JVM asks the OS to create a new native thread that will, on first schedule, call your run() method. The original thread continues immediately; the two now run concurrently.
public static void main(String[] args) {
System.out.println("main: hello from " + Thread.currentThread().getName());
Thread t = new Thread(() -> {
System.out.println("worker: hello from " + Thread.currentThread().getName());
}, "worker-1");
t.start(); // worker runs concurrently with main
System.out.println("main: continuing");
}The output interleaving is not deterministic — the OS decides which thread runs first, and that decision changes between runs. That non-determinism is the central fact of concurrent programming.
Why you'd use threads
Two distinct motivations, often conflated:
- Throughput. You have CPU-bound work — image resize, parsing, compression. One thread uses one core; eight threads use eight cores and finish roughly eight times faster. This is parallelism.
- Responsiveness. You have a thread that would otherwise block — waiting for a network reply, a database response, a user click. Putting that work on a separate thread lets the rest of the program keep doing useful things while it waits. This is concurrency.
Most real programs need both. A web server uses many threads so that one slow request doesn't stall the others (concurrency) and so that many fast requests can be handled in parallel across cores (parallelism).
Why threads are hard
Threads share memory. The same HashMap, ArrayList, or int counter++ can be touched by two threads at the same instant — and the JVM, the CPU caches, and the compiler are all allowed to reorder operations in ways that surprise you. The three problems multithreaded code keeps hitting:
- Race conditions. Two threads read-modify-write the same variable; one of the updates is lost.
counter++is not atomic — it's read counter, add one, write counter. Two threads can both read the same value and both write backvalue + 1, and you lose a tick. The fix is synchronization — forcing the read-modify-write to happen as one indivisible step. - Visibility. A thread writes a field; another thread reads it and sees the old value, because each thread has its own CPU cache and there's no rule that forces the write to propagate without a memory barrier. This is why
volatile,synchronized, andjava.util.concurrentexist. - Deadlock. Thread A holds lock X and waits for lock Y; thread B holds lock Y and waits for lock X. Neither ever proceeds. The program hangs with no exception and no log line. The deadlock chapter shows how to detect and avoid it.
The rest of this part of the book is largely about preventing these three failures while keeping the throughput gain.
The vocabulary you'll see
A few terms that show up everywhere and that the rest of the chapters assume:
| Term | What it means |
|---|---|
| Concurrency | Multiple tasks making progress over the same period of time. They may or may not literally run at the same instant. |
| Parallelism | Multiple tasks literally executing at the same instant on different cores. A subset of concurrency. |
| Mutual exclusion | Only one thread is allowed inside a critical section at a time. Locks and synchronized provide it. |
| Memory model | The rules that say when one thread is guaranteed to see another thread's write. Defined by the JLS, refined by JSR-133. |
| Atomic | An operation that cannot be observed half-done. Either it happened or it didn't — no in-between state visible to other threads. |
| Thread-safe | A class whose public API can be called from multiple threads without external synchronisation and still behave correctly. |
Daemon threads and the JVM exit rule
One thing that surprises beginners: the JVM exits when the last non-daemon thread finishes. The main thread is non-daemon. Threads you create with new Thread(...) are non-daemon by default — so spawning a worker thread keeps the JVM alive until that worker returns.
You can mark a thread as a daemon with t.setDaemon(true) before start(). Daemon threads don't keep the JVM alive; when all non-daemon threads finish, the JVM yanks them out from under. Use daemons for background work that should die with the program (a timer that polls, a metrics flusher) — never for work whose completion you actually need (file writes, transaction commits).
Threads vs. virtual threads
Java 21 introduced virtual threads, which look identical at the API level but are scheduled by the JVM on top of a small pool of OS threads. The mental model in this chapter — one Java Thread equals one OS thread — describes "platform threads," which is what you get with the unadorned new Thread(...) constructor. Platform threads are expensive: each one takes about 1 MB of native stack and the OS limits how many a process can have, so you create them with care. Virtual threads are cheap — millions are fine — and they make blocking I/O free again. We cover them in Java Virtual Threads; until then, "thread" means "platform thread."
A worked example: serial vs. parallel
The program below sums a chunk of CPU work two ways — once sequentially on the main thread, once split across four threads — and prints the wall-clock for each. The numbers vary by machine, but the shape is the same everywhere: more threads, less wall time, until you run out of cores.
What to take from the run:
- The serial run used one core; the parallel run used four. The speedup is sub-linear (closer to 3x than 4x) because the OS, the GC, and other JVM threads also want CPU time. Amdahl's law in action — a small serial fraction (the final sum-of-partials loop, the loop start-up) caps the speedup.
- Each worker wrote to its own slot in
partials[]. No two threads ever touched the same index, so no synchronisation was needed. This is the easiest form of parallelism — partition the data and let each thread own its partition. t.join()is howmainwaits forworker-3to finish. Without the joins, the loop would readpartialsbefore the workers had written, andparallelSumwould be wrong.joinis the one piece of thread coordination this program uses; the next chapters will introduce many more.- The daemon thread at the bottom didn't keep the JVM alive. It was about to sleep for 60 seconds, but
mainreturned and the JVM exited, killing the daemon mid-sleep without running its print statement. That's the daemon contract. Thread.currentThread().getName()and the explicit name passed to theThreadconstructor are how you tell threads apart in logs, in profilers, and in thread dumps. Always name your threads —Thread-3is useless when you're trying to figure out which one is stuck.
What's next
The next chapter, Java Thread Class, zooms in on the Thread object itself — its constructors, the difference between extending Thread and passing it a Runnable, and the API for naming, priority, daemon status, and interruption.