In this series, we’ll learn the what’s and how’s of threads in Java — from the basics of concurrency to practical multithreading patterns used in real-world systems.
What are threads?
A thread is the smallest unit of execution in a process. A Java program (JVM process) can contain many threads that execute code concurrently while sharing the same process memory. Threads run inside a process and share its heap and static data.
Each thread:
- Shares the process’s heap and static data.
- Has its own call stack and CPU register context.
Imagine a kitchen (process). Each cook is a thread. They share the same pantry (memory), but each cook can work on different tasks (methods, code) at the same time — prepping, stirring, plating.
⚡Why do we need threads?
Threads are mainly used to achieve parallelism and concurrency. When hardware has multiple CPU cores, multiple threads can run in parallel to shorten latency for CPU-bound tasks.
Thread Lifecycle
NEW — thread object created but not started yet.
RUNNABLE — executing on CPU or ready to run (OS-dependent; JVM treats this as runnable).
BLOCKED — waiting for a monitor lock (entering synchronized block/method).
WAITING — waiting indefinitely for another thread to perform a particular action (e.g., Object.wait() without timeout, Thread.join() without timeout).
TIMED_WAITING — waiting with a timeout (e.g., Thread.sleep(ms), wait(ms), join(ms)).
TERMINATED — finished execution (exited run()).

public class ThreadLifecycleDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("Inside run(): " + Thread.currentThread().getState());
try {
Thread.sleep(200); // TIMED_WAITING
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
System.out.println("After creation: " + t.getState()); // NEW
t.start();
System.out.println("After start: " + t.getState()); // RUNNABLE
t.join();
System.out.println("After join: " + t.getState()); // TERMINATED
}
}
Ways to create a thread
- Extending Thread :
You create a subclass of Thread and override run(). Starting the thread calls start() which eventually invokes your run() on a new OS-scheduled thread.
class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println(getName() + " started");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println(getName() + " interrupted");
}
System.out.println(getName() + " finished");
}
}
public class ExtendsThreadDemo {
public static void main(String[] args) {
Thread t = new MyThread("worker-1");
t.start(); // spawns a new thread
}
}
- Implementing Runnable :
You implement Runnable to describe the task. Then pass it to a Thread or—preferably—to an ExecutorService.
class Worker implements Runnable {
private final String name;
Worker(String name) { this.name = name; }
@Override
public void run() {
System.out.println(name + " started");
try { Thread.sleep(300); } catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(name + " finished");
}
}
public class RunnableDirectDemo {
public static void main(String[] args) {
Thread t = new Thread(new Worker("worker-2"));
t.start();
}
}
NOTE : Implementing Runnable is generally preferred over extending Thread to create a new thread because if a class extends thread, it can not extend any other class as java does not support multiple inheritance.