Command Palette

Search for a command to run...

PHASE 13Advanced ~33 min· topic 1 of 11

Topic 13.1

Threads and Runnable

In one line

A thread is an independent path of execution inside one program, with its own call stack but sharing the same heap. You describe the work as a Runnable and run it on a new Thread with start(); the JVM maps each such platform thread to an operating system thread.

Think of it like this

A restaurant kitchen. One cook can make a whole meal alone, one dish after another. Hire three cooks and the starter, the main and the dessert get made at the same time. They share one kitchen (the fridge, the stove, the counter), so they finish sooner, but they can also bump into each other or both grab the last egg. A Java program is the kitchen, each thread is a cook, and the shared kitchen is the heap, the memory where objects live.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Thread
One independent line of work inside a program. Several threads run at the same time and share the program's objects.
Concurrency
Several tasks making progress during the same period of time, possibly by taking turns on one core.
Parallelism
Several tasks running at literally the same instant, on different CPU cores.
Call stack
Each thread's private memory of which methods it is inside and their local variables.
Heap
The shared memory where all objects live. Every thread can reach an object on the heap if it has a reference to it.
Runnable
An interface with one method, void run(), that describes a piece of work to do.
Scheduler
The part of the operating system that decides which thread runs on which CPU core, and for how long.
join
A method that makes the current thread wait until another thread has finished.
Daemon thread
A background thread that doesn't stop the JVM from exiting. When only daemon threads are left, the program ends.
Platform thread
A Java thread backed one-to-one by an operating system thread. Before Java 21 every Java thread was one.

Step by step

01The main thread is already a thread

Before you create any thread, one is already running your code. Thread.currentThread() returns the Thread object for whoever is executing the current line.

The JVM also starts its own background threads (the garbage collector, the JIT compiler, a finalizer and others), but those are daemon threads you rarely see.

Main.javawhole filejava
public class Main {
    public static void main(String[] args) {
        Thread me = Thread.currentThread();
        System.out.println(me.getName());      // main
        System.out.println(me.isDaemon());     // false: main is a user thread
    }
}

02Describe the work, then start a thread

A Runnable is the job description. A Thread is the worker. start() returns immediately; the new thread begins running run() shortly after, on its own stack.

start() does real work: it asks the operating system for a new native thread, gives it a stack, and makes it runnable. The JVM then calls run() on that new thread. Your main continues straight to the next line without waiting.

Main.javawhole filejava
Runnable job = () -> System.out.println("hello from " + Thread.currentThread().getName());
Thread worker = new Thread(job, "worker-1");   // NEW: created, not running yet
worker.start();                                // now a real OS thread runs job.run()
worker.join();                                 // wait until it has finished
Describe the work, then start a threaddiagram
Rendering diagram…

03Each thread has its own stack, all share the heap

A local variable lives in a stack frame, and each thread has its own stack, so two threads running the same method have separate copies of its locals. That makes local variables naturally thread-safe.

An object is on the heap. If two threads hold a reference to the same array or the same ArrayList, they are looking at the same memory. Writing to shared objects without coordination is where races begin (Topic 13.3).

Each thread has its own stack, all share the heapdiagram
Rendering diagram…

04Split the work so threads don't collide

The safest way to share work is to give each thread its own slot to write to. In the first runnable example, worker i writes only partial[i], so no two threads write the same memory and no locking is needed.

main then joins every worker and adds the slots. The join is not optional: without it, main might read partial[2] before worker 2 has written it, and even if the timing worked, there would be no guarantee the write is visible. With it, both are guaranteed.

05Lambdas capture only effectively final locals

A lambda that runs on another thread may outlive the method that created it. Java therefore copies captured local variables into the lambda, and to keep that copy honest it only allows locals that never change after they're assigned.

Inside a loop, copy the loop variable first: final int id = i; and use id in the lambda. To return a result, write it into a shared object (an array slot, an AtomicInteger, a field) or use a Future (Topic 13.7).

terminal
$ javac Main.java
── expected output ──
Main.java:4: error: local variables referenced from a lambda expression must be final or effectively final
Thread t = new Thread(() -> count++);
^
1 error

06Order between threads is not guaranteed

If two threads print without coordination, the lines can come out in any order, and even lines from one thread can be separated by lines from another. The order you see on your laptop proves nothing about the server.

So when correctness depends on order, make the order explicit: join, a CountDownLatch (Topic 13.5), or a Future. The runnable examples in this phase do exactly that and print only after the work is done.

Main.javawhole filejava
Thread a = new Thread(() -> System.out.println("A"));
Thread b = new Thread(() -> System.out.println("B"));
a.start();
b.start();
// prints "A" then "B" on one run, "B" then "A" on another

07Daemon threads and the end of the program

The JVM exits when the last non-daemon thread ends (or someone calls System.exit). A user thread stuck in an infinite loop keeps the whole program alive, which is a common reason a program "doesn't finish".

Mark background helpers as daemons with setDaemon(true) before start(). They are killed at exit without running finally blocks, so never give a daemon thread work that must complete, like flushing a file.

08Platform threads are expensive

Each platform thread is an OS thread with a reserved stack (-Xss, commonly 1 MB on 64-bit Linux) and kernel bookkeeping. Creating one costs tens of microseconds, and a few thousand of them use a lot of memory and make the scheduler work hard.

That's why real code rarely writes new Thread(...). It hands tasks to a thread pool (Topic 13.6), which reuses a fixed set of threads, or, since Java 21, to virtual threads (Topic 13.10), which are cheap enough to create one per task.

Try it yourself

  1. 1

    Remove the joins

    In the first example, delete the line for (Thread t : workers) t.join();. Predict what the totals will show, then run it several times. You'll often see zeros or a wrong total, because main reads the slots before (or without seeing) the workers' writes.

  2. 2

    Use more threads

    Change the first example to 6 workers summing 1..6000 (each 1000 numbers). Predict the total with the formula n(n+1)/2 first, then run and compare.

  3. 3

    Call run() twice, start() twice

    In the second example, add a second t.start(); after the join(). Predict whether it compiles and what happens at run time, then check against the break-it section below.

Code & diagrams

Split a sum across three threads New tab

The workers may finish in any order, but main prints only after joining all of them, so the output never changes.

Sign in to run this example in your browser.

Expected output

main runs on: main
worker-0 summed 1..1000 = 500500
worker-1 summed 1001..2000 = 1500500
worker-2 summed 2001..3000 = 2500500
total 1..3000 = 4501500
formula n(n+1)/2 = 4501500
start() runs on a new thread, run() does not New tab
Sign in to run this example in your browser.

Expected output

t.run()   executed on: main
state after run(): NEW
t.start() executed on: helper
state after join(): TERMINATED
Three ways to give a thread its work New tab
Sign in to run this example in your browser.

Expected output

hello from subclass
runnable total = 55
lambda: lambda ran
An exception kills only its own thread New tab
Sign in to run this example in your browser.

Expected output

handler caught in worker: java.lang.IllegalStateException: boom
worker alive? false
main is still running
daemon? true, alive? true
main ends; the JVM does not wait for daemon threads
Java 21: thread builders Java 21+java
// Builders name and configure threads fluently (Java 21, JEP 444)
Thread t = Thread.ofPlatform()
        .name("importer-", 0)            // importer-0, importer-1, ... for each new thread
        .daemon(false)
        .start(() -> importFile());

ThreadFactory factory = Thread.ofPlatform().name("worker-", 1).factory();
ExecutorService pool = Executors.newFixedThreadPool(4, factory);   // named pool threads

Thread v = Thread.ofVirtual().start(() -> handleRequest());       // Topic 13.10

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Start the same thread twice

Call t.start(); a second time on a thread that has already been started.

terminal
$ java Main.java
── what you'll see ──
hi
Exception in thread "main" java.lang.IllegalThreadStateException
at java.base/java.lang.Thread.start(Thread.java:1525)
at Main.main(Main.java:5)

Break #2

Sleep inside a Runnable without handling the interrupt

Write Runnable r = () -> { Thread.sleep(100); };.

terminal
$ javac Main.java
── what you'll see ──
Main.java:4: error: unreported exception InterruptedException; must be caught or declared to be thrown
Thread.sleep(100);
^
1 error

Break #3

Change a local variable from a thread

Write int count = 0; Thread t = new Thread(() -> count++);.

terminal
$ javac Main.java
── what you'll see ──
Main.java:4: error: local variables referenced from a lambda expression must be final or effectively final
Thread t = new Thread(() -> count++);
^
1 error

Myth vs fact

Myth

Calling run() starts a thread.

Fact

Only start() creates a new thread. run() is an ordinary method call that executes on the current thread, so the work runs sequentially.

Myth

Threads run in the order you start them.

Fact

The OS scheduler decides. A thread started later can finish first. Use join, latches or futures whenever order matters.

Myth

More threads always means faster.

Fact

CPU-bound work can't go faster than the number of cores; extra threads only add switching and memory. More threads help when tasks spend their time waiting on I/O.

Myth

An exception in a thread crashes the program.

Fact

It kills only that thread and prints a stack trace to stderr. The program continues, often silently missing the work that thread was doing.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    On HotSpot, Thread.start() calls the native start0, which creates a pthread (Linux) or Win32 thread with a stack of -Xss (or ThreadStackSize) bytes, plus guard pages for stack-overflow detection. Hitting the OS limit on threads gives java.lang.OutOfMemoryError: unable to create native thread, which is about native memory and process limits, not the Java heap.

  • ▸

    Thread.start() happens-before every action in the started thread, and every action in a thread happens-before another thread's successful return from join() on it (JLS 17.4.5). That is why the first example is correct without volatile or locks: the writes to partial[i] are published by the join.

  • ▸

    Thread priorities map to OS priorities only on some platforms and are ignored by default on Linux (HotSpot needs -XX:ThreadPriorityPolicy and root). Never use priorities for correctness. Thread.yield() is a hint with no guaranteed effect, and Thread.stop() is deprecated for removal and throws UnsupportedOperationException since Java 20.

  • ▸

    Subclassing Thread couples the task to one execution mechanism and wastes your single superclass. Runnable (or Callable) lets the same code run on a pool, a virtual thread, a CompletableFuture or a scheduled executor without change.

Remember this

  1. 1

    Every Java program starts with one thread, called **main**, which runs your main method. A thread is a separate line of execution: it has its own call stack (its own local variables and its own chain of method calls, Topic 3.7) and its own program counter, but all threads in a JVM share the same heap. So a local int is private to one thread, while an object that two threads can both reach is shared. Almost every concurrency bug comes from that sharing.

  2. 2

    You describe *what* to do with a **Runnable**, a functional interface (Topic 10.1) with one method, void run(). You describe *who* does it with a **Thread** object: new Thread(runnable) creates it and **start()** asks the JVM to create a new operating system thread and call run() on it. Calling run() yourself is just an ordinary method call on the current thread: no new thread, no concurrency. That mistake compiles and runs, so it is easy to miss.

  3. 3

    There are three ways to supply the work: extend Thread and override run(), write a class that implements Runnable, or pass a lambda such as () -> doWork(). Prefer Runnable or a lambda: the task stays separate from the mechanism that runs it, so the same task can later go to an executor (Topic 13.6) or a virtual thread (Topic 13.10). A lambda can read local variables from the surrounding method only if they are effectively final (never reassigned), because the thread may still be running after the method that created it has returned.

  4. 4

    After start(), the order in which threads run is decided by the operating system's scheduler, not by your code. Two threads that each print a line can print in either order, and the order can change from run to run. To use a thread's result safely, wait for it with **join()**: when join() returns, the thread has finished and everything it wrote is visible to the thread that joined (a happens-before guarantee, Topic 13.4).

  5. 5

    Threads have a name (main, Thread-0, or one you choose; give them names, it makes stack dumps readable), a priority (a hint most operating systems ignore) and a daemon flag. The JVM exits when every non-daemon (user) thread has finished; daemon threads, such as background cleaners, don't keep it alive and are stopped abruptly at exit. setDaemon(true) must be called before start().

  6. 6

    An exception that escapes run() kills only that thread. The JVM prints Exception in thread "worker" ... to standard error and the rest of the program keeps going, which is how errors in background threads get lost. Install an **UncaughtExceptionHandler** to log them. Each platform thread also reserves memory for its stack (often 512 KB to 1 MB by default), so a program can't create millions of them; executors (Topic 13.6) reuse a few threads, and virtual threads (Topic 13.10) make threads cheap.

Explain it without notes

01

What is the difference between a process and a thread, and what do threads in the same JVM share?

02

Why does calling run() instead of start() not create concurrency?

03

Why can a lambda passed to a thread only use effectively final local variables?

04

What decides when the JVM exits, and what are daemon threads for?

Practice

01

Start four threads that each count the even numbers in a quarter of 1..4000 into their own array slot. Join them and print each count and the total.

02

Create a thread named printer whose task stores its own name and whether it is a daemon into a String[]. Start it, join it and print the stored values.

03

Write a thread whose task throws ArithmeticException (divide by zero). Install an uncaught exception handler that prints the exception class name, and print done after joining.

Trade-offs

  • ↔

    Raw Thread objects give full control (name, daemon flag, handler) but no reuse, no result values and no limit on how many you create. For anything beyond a demo, prefer an executor (Topic 13.6) or virtual threads (Topic 13.10).

  • ↔

    Splitting work into per-thread slots avoids locking entirely but needs work that divides cleanly. Shared mutable state is more flexible but needs synchronization (Topic 13.3), which costs speed and invites bugs.

  • ↔

    More threads help I/O-bound work (waiting on networks or disks) a lot, and CPU-bound work only up to the number of cores. Past that, context switches and memory use make things slower.

Done when you can

  • Done when you can create and start a thread with a lambda and wait for it with join.

  • Done when you can explain start() versus run() and what each thread shares and owns.

  • Done when you can pass results out of a thread safely (slots plus join).

  • Done when you know what daemon threads are and when the JVM exits.

  • Done when you handle exceptions in threads with an UncaughtExceptionHandler.