W3docs

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 the Runnable whose run() 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 back value + 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, and java.util.concurrent exist.
  • 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:

TermWhat it means
ConcurrencyMultiple tasks making progress over the same period of time. They may or may not literally run at the same instant.
ParallelismMultiple tasks literally executing at the same instant on different cores. A subset of concurrency.
Mutual exclusionOnly one thread is allowed inside a critical section at a time. Locks and synchronized provide it.
Memory modelThe rules that say when one thread is guaranteed to see another thread's write. Defined by the JLS, refined by JSR-133.
AtomicAn operation that cannot be observed half-done. Either it happened or it didn't — no in-between state visible to other threads.
Thread-safeA 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.

java— editable, runs on the server

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 how main waits for worker-3 to finish. Without the joins, the loop would read partials before the workers had written, and parallelSum would be wrong. join is 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 main returned 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 the Thread constructor are how you tell threads apart in logs, in profilers, and in thread dumps. Always name your threads — Thread-3 is 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.

Practice

Practice
Two threads each run `counter++` 100,000 times on a shared `int counter` that starts at 0. After both `join()`, what is the most likely value of `counter`?
Two threads each run `counter++` 100,000 times on a shared `int counter` that starts at 0. After both `join()`, what is the most likely value of `counter`?
Was this page helpful?