Java Threading
Prior to JDK 1.2, Java threading was implemented using green threads, which are user-level threads that operate independently of the operating system. In this model, the JVM simulated multithreading behavior rather than relying on native OS capabilities. Green threads had limitations such as inability to utilize asynchronous I/O or leverage multiple CPU cores effectively.
Starting from JDK 1.2, Java adopted native threads—the JVM now directly uses OS kernel threads for implementation, allowing the OS to manage thread scheduling and execution.
Relationship Between Threads and Processes
Memory Areas in JVM (Post-JDK 1.8)
Each process can contain multiple threads, all sharing heap and method area resources (metaspace in JDK 1.8+). However, each thread has its own program counter register, JVM stack, and native method stack.
Shared Resources: Heap and Method Area
The heap stores all newly created objects, while the method area holds loaded class metadata, constants, static variables, and compiled bytecode.
Private Resources: Program Counter, JVM Stack, and Native Method Stack
Program Counter Register
The program counter keeps track of the current instruction being executed. When switching between threads, it allows resumption at the correct execution point.
- Bytecode interpreter updates the program counter to control flow.
- In multi-threading scenarios, it records the last executed position per thread.
- For native methods, the value is undefined; otherwise, it points to the next bytecode instruction.
JVM Stack and Native Method Stack
The JVM stack handles Java method execution by creating frames containing local variables, operand stacks, and runtime constant pool references. Each method call corresponds to a frame entering and exiting the stack.
The native method stack supports native method calls. In HotSpot VM, both stacks are merged into one, ensuring thread isolation through private memory allocation.
Thread States in Java
Every thread exists in exactly one of six possible states:
- NEW: Thread created but not yet started via
start() - RUNNABLE: Thread ready to execute, waiting for CPU time
- BLOCKED: Waiting to acquire a monitor lock
- WAITING: Waiting indefinitely for another thread's action
- TIME_WAITING: Waiting for a specified duration before returning
- TERMINATED: Thread completed execution
Common Thread Creation Methods
There are three standard approaches to create threads:
Implementing Runnable Interface
This approach defines a task that can be run in a thread but does not constitute a thread itself.
public class TaskExample implements Runnable {
@Override
public void run() {
System.out.println("Running task in thread");
}
}
public class Main {
public static void main(String[] args) {
TaskExample task = new TaskExample();
Thread worker = new Thread(task);
worker.start();
}
}
Using Callable Interface
Unlike Runnable, Callable supports return values wraped in FutureTask.
import java.util.concurrent.*;
public class TaskWithResult implements Callable<Integer> {
public Integer call() {
return 42;
}
}
public class Main {
public static void main(String[] args) throws Exception {
TaskWithResult task = new TaskWithResult();
FutureTask<Integer> future = new FutureTask<>(task);
Thread executor = new Thread(future);
executor.start();
System.out.println(future.get());
}
}
Extending Thread Class
Direct subclassing of Thread is another way to define custom thread behavior.
public class CustomThread extends Thread {
@Override
public void run() {
System.out.println("Custom thread running");
}
}
public class Main {
public static void main(String[] args) {
CustomThread thread = new CustomThread();
thread.start();
}
}
Calling run() directly instead of start() executes the method within the calling thread context, not as a separate thread.
Thread Interruption
Thread interruption is a flag indicating that a thread should stop or handle cancellation. While normal execution remains unaffected, certain blocking operations react to interruptions.
Impact on Blocking Operations
When a thread is sleeping or waiting, calling interrupt() causes an InterruptedException. Before throwing the exception, the JVM clears the interrupt status. Subsequently, isInterrupted() returns false.
Wait/Notify Mechanism
If a thread waits on an object and receives an interrupt signal, it throws InterruptedException immediately. This differs from normal wake-up because interrupted threads exit the wait block in to catch clauses rather than continuing execution after wait().
Sleep and Join
To awaken a sleeping or joining thread early, use interrupt(). This transfers control to the exception handler where further logic can be managed.
Inter-thread Communication
Volatile Keyword
Variables marked volatile ensure visibility across threads. Without volatile, changes made by one thread may not be visible to others due to caching issues.
Synchronized Keyword
Used to protect critical sections. Only one thread can execute synchronized blocks or methods at a time, providing mutual exclusion and visibility guarantees.
Wait/Notify Pattern
The wait(), notify(), and notifyAll() methods allow coordination between threads. A thread calling wait() releases its lock and enters waiting state until notified.
Thread.join()
When thread A calls B.join(), A waits until B completes execution. Overloads support timeouts via join(long) or join(long, int).