Threading Basics in Java
What is a Thread?
A thread is the smallest unit of execution within a process. Every Java application starts with a single thread — the main thread — and can spawn additional threads to perform work concurrently.
A process owns memory, file handles, and other resources. Threads within the same process share that memory space, which makes communication between them fast but also introduces the challenge of coordinating access to shared data.
Thread Lifecycle

A thread moves through these states during its lifetime:
- NEW — Thread object is created but
start()has not been called yet. - RUNNABLE — The thread is eligible to run. The OS scheduler decides when it actually executes.
- RUNNING — The thread is actively executing on a CPU core.
- BLOCKED — The thread is waiting to acquire a monitor lock (e.g., entering a
synchronizedblock held by another thread). - WAITING — The thread is parked indefinitely until another thread explicitly wakes it.
- TIMED_WAITING — Similar to WAITING but with a timeout.
- TERMINATED — The thread’s
run()method has completed or an unhandled exception killed it.
Creating Threads
Extending Thread
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Running in: " + Thread.currentThread().getName());
}
}
// Usage
MyThread thread = new MyThread();
thread.start(); // Never call run() directly — that executes on the current thread
Implementing Runnable
public class MyTask implements Runnable {
@Override
public void run() {
System.out.println("Running in: " + Thread.currentThread().getName());
}
}
// Usage
Thread thread = new Thread(new MyTask());
thread.start();
Lambda syntax (Java 8+)
Thread thread = new Thread(() -> {
System.out.println("Running in: " + Thread.currentThread().getName());
});
thread.start();
Prefer Runnable over extending Thread. Java doesn’t support multiple inheritance, so extending Thread prevents you from extending anything else. Composition is cleaner.
Key Thread Methods
| Method | Description |
|---|---|
start() | Schedules the thread for execution. Calls run() on a new OS thread. |
run() | Contains the code the thread will execute. Never call directly. |
sleep(long ms) | Pauses the current thread for at least ms milliseconds. |
join() | Blocks the calling thread until this thread terminates. |
interrupt() | Sends an interrupt signal to the thread. |
isInterrupted() | Checks whether the thread has been interrupted. |
yield() | Hints to the scheduler that the thread is willing to give up its time slice. |
setDaemon(true) | Marks the thread as a daemon — JVM exits when only daemons remain. |
Thread Interruption
Interruption is a cooperative mechanism. It doesn’t force a thread to stop. Instead it sets a flag and, if the thread is sleeping or waiting, throws InterruptedException.
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// Do useful work...
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// Restore the interrupted flag and exit gracefully
Thread.currentThread().interrupt();
break;
}
}
System.out.println("Worker cleaned up and exiting.");
});
worker.start();
// Later, signal the worker to stop
Thread.sleep(500);
worker.interrupt();
Key points:
- If a thread is blocked in
sleep(),wait(), orjoin(), interruption throwsInterruptedExceptionand clears the flag. - If a thread is doing computation, it must periodically check
isInterrupted()itself. - Always restore the interrupt flag after catching
InterruptedExceptionif you don’t immediately terminate.
Daemon vs Non-Daemon Threads
- Non-daemon (default): The JVM stays alive as long as any non-daemon thread is running.
- Daemon: Background threads that don’t prevent JVM shutdown. Garbage collector is a daemon thread.
Thread daemon = new Thread(() -> {
while (true) {
// Background work — will be killed when JVM exits
}
});
daemon.setDaemon(true);
daemon.start();
Set daemon status before calling start().
Thread Pools and ExecutorService
Creating a new OS thread for every task is expensive. Thread pools reuse a fixed set of threads:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
final int taskId = i;
pool.submit(() -> {
System.out.println("Task " + taskId + " on " + Thread.currentThread().getName());
});
}
pool.shutdown(); // Gracefully finish pending tasks, then stop
Common pool types:
| Factory Method | Behavior |
|---|---|
newFixedThreadPool(n) | Exactly n threads. Tasks queue up if all are busy. |
newCachedThreadPool() | Creates threads on demand, reuses idle ones. Good for short tasks. |
newSingleThreadExecutor() | One thread. Tasks execute sequentially. |
newScheduledThreadPool(n) | Supports delayed and periodic tasks. |
newVirtualThreadPerTaskExecutor() | One virtual thread per task (Java 21+). |
Atomicity of Primitive Operations
Not all operations on primitives are atomic:
| Operation | Atomic? |
|---|---|
Reading/writing int, short, byte, char, float, boolean | Yes |
| Reading/writing object references | Yes |
Reading/writing long and double | No — two 32-bit operations |
volatile long / volatile double | Yes — forced single operation |
i++, i += 1 | No — read + modify + write |
The volatile keyword ensures visibility across threads and atomic reads/writes for long and double, but does not make compound operations (like increment) atomic.
Memory Visibility Problem
Without synchronization, changes made by one thread may not be visible to another thread. Each CPU core may cache values locally:
// Thread 1
boolean running = true;
// Thread 2 sets running = false, but Thread 1 never sees it
// because Thread 1 reads from its CPU cache, not main memory
while (running) {
// Infinite loop — never sees the update
}
volatile fixes this by ensuring reads always go to main memory:
volatile boolean running = true; // Now visible across threads
Summary
- Threads share process memory, making inter-thread communication fast but error-prone.
- Use
Runnable+ thread pools over rawThreadsubclassing. - Interruption is cooperative — threads must check and respond.
- Primitive reads/writes are mostly atomic, except
long/doublewithoutvolatile. volatileensures visibility but not atomicity of compound operations.- The real challenges begin when multiple threads access shared mutable state — that’s where synchronization comes in.