An thread is a sequence of execution that can advance concurrently with others.
Until now, your programs have been sequential. Like a cook who cuts the onion, then turns on the stove, and then beats eggs. If they wait without doing anything while the water boils, they are blocked.
A team can have multiple cores, but concurrency and parallelism are not synonymous. Several tasks are concurrent if they make progress during the same period; they are only parallel if they execute physically at the same time.
Java represents each thread using Thread and allows separating the task using Runnable.
What is a thread?
A thread maintains its own call stack and shares the heap with other threads of the process. When an application starts, the main thread appears, along with other internal threads that the JVM may create.
To do things in parallel (like downloading a file while the graphical interface keeps responding), we need to create additional threads. Java gives us two ways to do this.
Inheriting from the Thread class
We can create a class that directly extends java.lang.Thread and override its run() method.
// 1. Define the thread
class MyThread extends Thread {
@Override
public void run() {
System.out.println("I am a thread running in parallel");
}
}
// 2. Start it
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // Starts!
}
}
Why is it NOT recommended?
- Single inheritance: In Java you can only inherit from one class. If your class extends
Thread, it can no longer extendUserorWindow. You spend your only inheritance slot on something technical. - Poor design: You mix the task (the code to execute) with the execution mechanism (the thread).
Implementing Runnable
The Runnable interface represents a “task”. It is a functional interface (a single method: run()).
This allows us to separate WHAT we want to do (the task) from WHO is going to do it (the thread).
// 1. Define the task (Runnable)
class HeavyTask implements Runnable {
@Override
public void run() {
System.out.println("Doing complex calculations...");
}
}
// 2. Pass the task to a generic Thread
public class Main {
public static void main(String[] args) {
HeavyTask task = new HeavyTask();
Thread thread = new Thread(task); // "Here, do this"
thread.start();
}
}
Modern version (lambdas)
Since Runnable is a functional interface, we can avoid the extra class and use a lambda. It’s a common way to express short tasks.
Thread thread = new Thread(() -> {
System.out.println("Hello from a parallel Lambda!");
System.out.println("Current thread: " + Thread.currentThread().getName());
});
thread.start();
Virtual threads and platform threads
Since Java 21, Thread can represent platform threads or virtual threads. Virtual threads are scheduled by the JVM on a reduced set of platform threads and are especially useful for many tasks that spend time blocked on I/O.
Thread virtual = Thread.startVirtualThread(() -> {
System.out.println("Hello from a virtual thread");
});They don’t make computation use more cores by themselves: for CPU-intensive work, the limit is still the available cores.
Difference between start() and run()
This is the most famous interview question about threads.
thread.run(): On a platform thread created withnew Thread(task), it is a normal call and executes the task in the current thread. On a virtual thread, callingrun()directly does not start nor execute its task.thread.start(): Schedules a new thread and makes it invokerun(). On a platform thread, the operating system gets involved; on a virtual one, the JVM schedules it on its carrier threads.
Thread t = new Thread(() -> System.out.println("Hello"));
t.run(); // BAD ❌: Executes "Hello" in the Main thread (sequential).
t.start(); // GOOD ✅: Creates a new thread and executes "Hello" in parallel.
The lifecycle of a thread
A thread doesn’t just “exist”. It goes through several states from birth to death.
NEW: You just created the Thread, but it hasn’t started yet.
RUNNABLE: It is executing or ready to receive CPU time; Java groups both situations in this state.
BLOCKED: Waiting to enter a section protected by a monitor.
WAITING / TIMED_WAITING: Waiting for another action, with or without a time limit.
TERMINATED: The run() method has finished and the thread cannot be restarted.
The chaos of concurrency
Creating threads is easy. The hard part is that you don’t know when they will execute.
The Operating System’s Thread Scheduler decides who gets CPU time and when. It can pause your thread in the middle of an addition.
// Example of indeterminism
Thread t1 = new Thread(() -> System.out.println("A"));
Thread t2 = new Thread(() -> System.out.println("B"));
t1.start();
t2.start();
Possible output 1: A then B.
Possible output 2: B then A.
Never assume the order.