← Back to the section

When a program runs, the operating system gives it resources: memory, file access, CPU time. To do several things at once, you need to understand how processes and threads work.

Process and thread: what's the difference

A process is a running program. Every process has its own isolated memory: one process cannot accidentally touch another's data. When you launch a Java application with java -jar app.jar, the operating system creates a single process.

A thread is a unit of execution inside a process; a process may hold one or many. All threads of the same process share the same memory area: the heap, static fields, open files. This is what makes threads a powerful tool — and a source of bugs if you use them carelessly.

A short formula: a process is a container of resources, a thread is a unit of work inside that container.

Why you need multithreading

Imagine a server that handles HTTP requests. Single-threaded, the second request waits until the first one finishes — even though the first mostly just waits for a response from the database.

Multithreading solves two problems:

  • Responsiveness. While one thread waits for I/O (network, disk, database), another thread keeps working. The program does not "freeze".
  • Core utilization. Modern CPUs have 4, 8, 16 or more cores. A single-threaded program uses one core. Threads let you engage all available cores for parallel computation.

How to start a thread: Thread and Runnable

Java has two basic ways to describe a task.

Way 1 — extending Thread:

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running: " + Thread.currentThread().getName());
    }
}

Thread t = new MyThread();
t.start(); // starts a new thread; run() executes in it

Way 2 — implementing Runnable (preferred):

Runnable task = () -> System.out.println("Thread running: " + Thread.currentThread().getName());

Thread t = new Thread(task);
t.start();

Runnable is preferred: the class does not "spend" its single inheritance on Thread, and the task stays separated from the mechanism that runs it.

start() vs run(): a common mistake

A frequent mistake is calling run() instead of start(). The code looks fine, nothing fails — there simply is no second thread.

t.run() no new thread main run() main waits while the work runs t.start() work moves to its own thread main join() run() worker-1 works, main moves on

Left: run() is an ordinary method call, the work happens right inside main, and main waits while it runs. Right: start() makes the JVM spin up a second thread, main moves on without waiting, and join() brings the lines back together.

The thread name in the output gives it away:

live example

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> System.out.println("running in thread " + Thread.currentThread().getName());

        Thread t1 = new Thread(task, "worker-1");
        t1.run();   // wrong: an ordinary method call

        Thread t2 = new Thread(task, "worker-2");
        t2.start(); // right: the JVM creates a thread and calls run() in it
        t2.join();
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

It prints main, then worker-2: after t1.run() the thread name did not change, because that is an ordinary method call. start() asks the JVM to create a system thread and call run() inside it.

Waiting for completion: join()

start() returns immediately — the new thread lives on its own. If you need its result, the calling thread must wait explicitly, with join().

live example

public class JoinDemo {
    public static void main(String[] args) throws InterruptedException {
        int[] result = new int[1];
        Thread worker = new Thread(() -> {
            try {
                Thread.sleep(100);           // long-running work
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }
            result[0] = 42;
        });

        worker.start();
        System.out.println("right after start(): " + result[0]);
        worker.join();
        System.out.println("after join(): " + result[0]);
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

The first line prints 0 — the work has not finished yet; the second prints 42, because join() blocked main until worker finished. A timeout is possible: worker.join(5000) waits no longer than 5 seconds.

Daemon threads

A daemon thread is a background thread that the JVM does not wait for when the program ends. Once all non-daemon threads have finished, the JVM shuts down on its own, aborting all daemons.

live example

public class DaemonDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread cleaner = new Thread(() -> {
            while (true) {
                System.out.println("cleaning the cache");
                try {
                    Thread.sleep(150);
                } catch (InterruptedException e) {
                    return;
                }
            }
        });
        cleaner.setDaemon(true);   // set BEFORE start()
        cleaner.start();

        Thread.sleep(400);
        System.out.println("main thread finished — the JVM exits and kills the daemon");
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

The loop prints "cleaning the cache" a few times, but as soon as main ends the JVM exits and cuts the daemon off mid-flight. Without setDaemon(true) the program would never finish. Daemons carry garbage collection, monitoring, cache cleanup; user work stays in regular threads.

Thread states

Every thread has a lifecycle. You can get it via t.getState():

diagram
  • NEW — the thread is created, start() has not been called yet.
  • RUNNABLE — running or ready to run (the OS scheduler decides when to give it the CPU).
  • BLOCKED / WAITING / TIMED_WAITING — the thread is waiting for something (a monitor, a notification, a timeout).
  • TERMINATED — run() finished.

One arrow looks surprising: after notify() a thread goes to BLOCKED, not straight to RUNNABLE. It gave the monitor up to sleep in wait(), and the monitor is still held by whoever called notify() — so the woken thread queues up for it again, and in a thread dump that looks exactly like BLOCKED.

The cost of an OS thread

Creating a thread is not cheap. Every OS thread requires:

  • allocating a stack (usually 512 KB — 1 MB by default);
  • a system call to create the thread;
  • scheduler overhead for context switching.

In practice: do not create a thread per request in a loop. To manage a pool, use ExecutorService — covered in the article on thread pools.

Thread vs task

A thread is an OS mechanism. A task (Runnable, Callable) is what you want to execute — different things, not to be conflated.

ThreadTask
What it isOS resourceWork logic
Creationnew Thread(...)lambda, class
Who manages itJVM + OSprogrammer / pool
Costhighlow

Good practice: describe tasks (Runnable / Callable) and hand them to a pool (ExecutorService) instead of managing threads by hand. Managing them yourself stays relevant mainly for understanding the fundamentals.

Virtual threads (Java 21)

Java 21 brought virtual threads — lightweight threads managed by the JVM rather than the OS. You can create them by the millions without exhausting system resources:

Thread vt = Thread.ofVirtual().start(() -> {
    System.out.println("virtual thread");
});

Virtual threads are ideal for I/O-bound tasks. For CPU-intensive computation you still use regular threads. More details in the article on virtual threads.

In short

  • A process is an isolated running program; a thread is a unit of execution inside it, sharing memory.
  • Multithreading gives responsiveness (not waiting on I/O) and use of all CPU cores.
  • start() creates a new thread, run() is a method call in the current one; Runnable beats extending Thread.
  • join() blocks the caller until another thread finishes; the JVM does not wait for daemons on exit.
  • Creating an OS thread is expensive — stack, system call, context switch; under load you take ExecutorService.
  • Java 21 gives you virtual threads — a lightweight alternative for I/O scenarios.