Modern applications leverage multithreading to execute concurrent tasks efficiently. A thread represents the smallest unit of execution within a process, enabling parallelism even on single-core systems through time-slicing. Understanding how threads are created, managed, and synchronized is critical for building responsive and scalable Java applications.
Thread Lifecycle States
- New: A Thread object is instantiated but not yet started.
- Runnable: The thread is eligible to run after calling
start(), awaiting CPU allocation. - Running: The thread is actively executing its
run()method. - Blocked: The thread is temporari paused due to:
- Waiting: Invoking
wait()on an object monitor. - Synchronized: Attempting to acquire a locked monitor via
synchronizedorLock. - Sleeping: Calling
Thread.sleep()— does not release locks. - Yielding: Voluntarily relinquishing CPU via
yield().
- Waiting: Invoking
- Terminated: The thread completes execution of
run()or is forcibly stopped (deprecated methods likestop()should be avoided).
Blocked threads transition back to Runnable when conditions are met: sleep duration expires, I/O completes, or notify()/notifyAll() is invoked on the monitor object.
Thread Creation Approaches
1. Extending Thread Class
Direct subclassing of java.lang.Thread overrides the run() method.
class Worker extends Thread {
@Override
public void run() {
System.out.println("Executing via Thread extension");
}
}
// Usage
Worker worker = new Worker();
worker.start();
2. Implementing Runnable
More flexible than inheritance; separates task logic from thread lifecycle.
class Task implements Runnable {
@Override
public void run() {
System.out.println("Executing via Runnable");
}
}
// Usage
Task task = new Task();
Thread thread = new Thread(task);
thread.start();
3. Implementing Callable
Similar to Runnable but supports return values and checked exceptions via call().
import java.util.concurrent.*;
class ComputableTask implements Callable<String> {
@Override
public String call() throws Exception {
return "Result from Callable task";
}
}
// Usage with ExecutorService
ExecutorService pool = Executors.newFixedThreadPool(1);
Future<String> future = pool.submit(new ComputableTask());
String result = future.get(); // Blocks until completion
pool.shutdown();
4. Using Thread Pools
Recommended for production systems to avoid excessive thread creation.
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> System.out.println("Task executed via pool"));
executor.shutdown();
Synchronization and Thread Safety
Using ReentrantLock
Explicit locking provides finer control than synchronized blocks.
import java.util.concurrent.locks.ReentrantLock;
class SafeCounter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
Using synchronized
Implicit locking on object monitors. Static methods lock on class; instance methods lock on this.
class SyncCounter {
private static int counter = 0;
public static synchronized void increment() {
counter++;
}
}
Inter-Thread Communication
Threads coordinate using shared state and signaling mechanisms:
- Wait/Notify: Threads wait on object monitors and are signaled via
notify()ornotifyAll(). - Condition Variables: Used with
Lockobjects for more precise control thanwait/notify. - Shared Variables: Access to volatile or atomic variables ensures visibility across threads.
Deadlock Detection
Using jstack
Run jps to identfiy Java process IDs, then use jstack <pid> to dump thread states. Deadlocks are explicitly flagged with stack traces showing circular waits.
Using jconsole
The GUI tool jconsole (found in JDK’s bin directory) provides real-time monitoring of thread states and detects deadlocks visually under the "Threads" tab.
Thread Pool Types
newCachedThreadPool(): Creates threads as needed; reuses idle ones.newFixedThreadPool(n): Maintains exactlynactive threads.newSingleThreadExecutor(): Serial execution with one thread.newScheduledThreadPool(n): Supports delayed and periodic tasks.newWorkStealingPool(): Uses fork-join framework for work stealing (default parallelism = CPU cores).
For full control, instantiate ThreadPoolExecutor directly:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // corePoolSize
8, // maximumPoolSize
60L, // keepAliveTime
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
Why Use Thread Pools?
Thread creation and destruction are expensive. Pools reduce overhead by reusing threads, limit resource consumption, and provide structured task queuing and rejection policies. They also simplify lifecycle management and improve application stability under load.
Deadlock Example
A classic deadlock occurs when two threads hold resources the other needs.
public class DeadlockExample {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread 1: Locked A");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
synchronized (lockB) {
System.out.println("Thread 1: Locked B");
}
}
}).start();
new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread 2: Locked B");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
synchronized (lockA) {
System.out.println("Thread 2: Locked A");
}
}
}).start();
}
}