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 contain one thread 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 application that handles HTTP requests. If it is single-threaded, the second request waits until the first one fully finishes — even when the first spends most of its time simply waiting 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 for a thread.
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
This is one of the most frequent slip-ups for beginners.
Thread t = new Thread(() -> System.out.println("hello"));
t.run(); // WRONG — runs in the CURRENT thread, not in a new one
t.start(); // CORRECT — the JVM creates a new thread and calls run() in it
Calling run() directly is an ordinary method call. No new thread is created. start() asks the JVM to create a system thread and call run() in it.
Waiting for completion: join()
If the main thread must wait for the result of another thread, use join():
Thread t = new Thread(() -> {
// long-running work
computeResult();
});
t.start();
// the main thread keeps doing something...
doOtherWork();
t.join(); // wait until thread t finishes
System.out.println("done"); // runs only after t completes
join() blocks the calling thread until the specified thread finishes execution. You can pass a timeout: t.join(5000) — wait no longer than 5 seconds.
Daemon threads
A daemon (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.
Thread daemon = new Thread(() -> {
while (true) {
cleanupExpiredCache(); // background task
Thread.sleep(60_000);
}
});
daemon.setDaemon(true); // set BEFORE start()
daemon.start();
Typical examples of a daemon are the garbage collector, a monitoring thread, periodic cache cleanup. Regular threads (non-daemons) are user work: HTTP requests, transactions.
Thread states
Every thread has a lifecycle. You can get it via t.getState():
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()has finished.
The cost of an OS thread
Creating a thread is not a cheap operation. 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 this means: do not create threads in a loop for every request. To manage a pool of threads, use ExecutorService — covered in the article on executors.
Thread vs task
A thread is an OS mechanism. A task (Runnable, Callable) is what you want to execute. These are different things, and you should not conflate them.
| Thread | Task | |
|---|---|---|
| What it is | OS resource | Work logic |
| Creation | new Thread(...) | lambda, class |
| Who manages it | JVM + OS | programmer / pool |
| Cost | high | low |
A good practice is to describe tasks (Runnable / Callable) and hand them to a pool (ExecutorService) rather than manage threads by hand. Manual thread management stays relevant only for understanding the fundamentals and for specific cases.
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 risking exhaustion of 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 a process with shared memory.
- Multithreading is needed for responsiveness (not waiting on I/O) and for utilizing all CPU cores.
Runnablewithnew Thread(task)is preferable to extendingThread.start()creates a new thread;run()is just a method call in the current thread.join()blocks the calling thread until another one finishes.- A daemon thread does not keep the JVM from shutting down.
- Creating an OS thread is expensive; under real load use
ExecutorService. - Java 21 gives you virtual threads — a lightweight alternative for I/O scenarios.
What to read next
- Java Memory Model — how threads see each other's changes and what
happens-beforeis. - Data races — what happens when two threads read and write the same variable without synchronization.
- Executors and thread pools — how to manage tasks through
ExecutorServiceinstead of creating threads by hand.