← Back to the section

CONCURRENCY Phase 6. Threads and processes

Threads and Processes in Java

Java

Thread, Runnable, start versus run, join, interruption and virtual threads.

1. Processes isolate memory, threads share it

1 / 5

A process is a separate program with resources. Threads inside it execute code concurrently and see the same objects.

  • Thread switching is cheaper than process switching.
  • Shared memory speeds exchange but creates races.
  • Independent work is often better as a separate task.
process - shared memory T1 T2 T3 shared objects

2. Thread receives work through Runnable

2 / 5

Runnable describes work without a result, Thread executes it separately. Callable also returns a value and can throw a checked exception.

  • Avoid extending Thread unnecessarily.
  • The task and execution mechanism are separate.
  • ExecutorService accepts Runnable and Callable.
Runnable Thread Callable Future

3. start creates a thread, run stays in the current one

3 / 5

start() asks the JVM for a new thread. Calling run() directly is a normal method call in the same thread, with no concurrency.

  • A thread cannot be started twice.
  • The run body executes once per task.
  • Errors travel through a handler or Future.
main start() new thread run() - just a method in main

4. join waits, interrupt asks for cancellation

4 / 5

join() blocks the caller until the thread ends. interrupt() sets a flag that the task checks to finish itself.

  • join(timeout) is available.
  • InterruptedException clears the flag.
  • Interrupt does not kill a thread.
main: join worker stop interrupt - a request, not a kill

5. Thread states and the virtual-thread model

5 / 5

NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING and TERMINATED describe the lifecycle. Virtual threads allow waiting without many OS threads.

  • BLOCKED waits for a monitor, WAITING waits for another thread.
  • Daemon threads do not keep the JVM alive.
  • OS threads are expensive, virtual threads are lightweight.
NEW RUNNABLE TERMINATED many VT few OS

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.