A process = an instance of a Java program (e.g., the JVM process).
A thread = a path of execution within that process.
Threads let you:
Do multiple things at once (concurrency).
Improve performance on multi-core CPUs (parallelism).
Keep programs responsive (e.g., GUI, servers, async I/O).
class MyThread extends Thread {
public void run() {
System.out.println("Thread running...");
}
}
public class Main {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start(); // starts a new thread
}
}
start() creates a new thread and calls run() internally.
class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread running...");
}
}
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
}
}
This approach is preferred because it separates the task (Runnable) from the thread control (Thread).
A thread goes through a series of states:
NEW: Thread object created but not started yet.
Calls start() → moves to RUNNABLE.
RUNNABLE: Thread is ready to run or running (OS scheduler decides).
yield(), sleep(), wait() → temporarily paused.
BLOCKED: Waiting for a monitor lock (e.g., entering synchronized block).
Lock released → back to RUNNABLE.
WAITING: Waiting indefinitely for another thread’s action (e.g., wait(), join()).
notify(), notifyAll(), or join() completion → RUNNABLE.
TIMED_WAITING: Waiting for a specified time (e.g., sleep(1000), wait(1000)).
Timeout or notification → RUNNABLE.
TERMINATED: Thread has finished execution (run() exits). End of life cycle.
Tells the scheduler: "I’m willing to let other threads run now."
But the thread remains in RUNNABLE state, not BLOCKED or WAITING.
Does not release any locks
Can be called anywhere in code
Can throw InterruptedException
Must be called inside synchronized block
Can throw InterruptedException
synchronized(obj) {
obj.wait(); // releases obj lock, waits for notify
}
notify(): Wakes one waiting thread on the same object.
notifyAll(): Wakes all waiting threads on the same object.
synchronized(obj) {
obj.notify(); // wakes one waiting thread
obj.notifyAll(); // wakes all waiting threads
}
Typical pattern:
synchronized(queue) {
while(queue.isEmpty()) {
queue.wait(); // wait for producer
}
queue.remove(); // consume item
queue.notify(); // notify producer
}
Current thread blocks until the target thread terminates.
Thread t1 = new Thread(() -> System.out.println("Thread 1 done"));
t1.start();
t1.join(); // main thread waits here until t1 finishes
System.out.println("Main thread resumes");
Can also specify a timeout: t1.join(1000) → wait max 1 second.
Common use case: ensure sequential execution of threads when needed.
It does not forcibly stop a thread — it merely sets a flag (the thread’s interrupt status), which the thread can check or react to.
Every thread has an interrupt flag (a boolean stored inside the Thread object).
Example: Interrupting a Sleeping Thread
Thread t = new Thread(() -> {
try {
Thread.sleep(5000);
System.out.println("Finished sleeping");
} catch (InterruptedException e) {
System.out.println("Sleep interrupted!");
}
});
t.start();
Thread.sleep(1000);
t.interrupt();
Check its interrupt status (Thread.interrupted() or isInterrupted()).
isInterrupted(): Instance method → Checks if current thread is interrupted (does not clear flag).
Thread.interrupted(): Static method → Checks and clears the interrupt flag.
React to the interruption if it’s blocking or sleeping.
Example: Checking Interrupt Flag Manually
class MyTask implements Runnable {
public void run() {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("Working...");
// simulate work
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("Interrupted while sleeping!");
// re-set flag: so the already cleared flag set to interrupted.
// So any other logic/code knows that this thread is interrupted.
Thread.currentThread().interrupt();
break;
}
}
System.out.println("Thread exiting gracefully.");
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new MyTask());
t.start();
Thread.sleep(2000);
t.interrupt(); // signal the thread to stop
}
}
Interrupting a non-blocked thread:
If the thread is just running normally (not in sleep, wait, or join), calling interrupt() simply sets the flag — nothing else happens.
The thread must check the flag periodically and exit on its own.
Interrupting a blocked thread:
If the thread is waiting in:
Thread.sleep()
Object.wait()
Thread.join()
LockSupport.park()
or Condition.await()
→ these methods throw InterruptedException and automatically clear the interrupt flag.
You must handle it in a catch block.
The keyword volatile mark a variable as being stored in main memory, not just in a thread’s local cache.
By default, each thread in Java can cache variables for performance. So, if one thread updates a variable, another thread might not see the latest value because it’s still using its cached copy.
When a variable is declared volatile:
Read: Always reads its value directly from main memory.
Write: Always writes its value directly to main memory.
Visibility guarantee: Changes made by one thread become immediately visible to others.
For example:
volatile int counter = 0;
counter++; // Not atomic!
This line actually does three steps:
Read counter
Increment value
Write back
If two threads do this at the same time, both may read the same value before incrementing → lost updates.
class Worker extends Thread {
private volatile boolean running = true;
public void run() {
while (running) {
// do work
}
System.out.println("Stopped.");
}
public void stopRunning() {
running = false;
}
}
Here, volatile ensures when another thread calls stopRunning(), the run() loop sees running = false immediately.
class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // First check (no locking)
synchronized (Singleton.class) {
if (instance == null) { // Second check (with lock)
instance = new Singleton();
}
}
}
return instance;
}
}
volatile prevents reordering of object construction instructions, which could otherwise cause threads to see a partially constructed object.
A deadlock is a situation in which two or more threads are blocked forever, each waiting for a resource held by another thread.
Deadlock occurs only if all these four conditions hold (Coffman conditions):
Mutual Exclusion: Only one thread can hold a resource at a time.
Hold and Wait: Thread holds one resource while waiting for another.
No Preemption: Resource cannot be forcibly taken from a thread.
Circular Wait: There exists a cycle of threads, each waiting for a resource held by the next thread.
Remove any one condition → no deadlock.
class DeadlockDemo {
static Object lock1 = new Object();
static Object lock2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized(lock1) {
System.out.println("Thread 1: Holding lock1...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized(lock2) {
System.out.println("Thread 1: Holding lock1 & lock2...");
}
}
});
Thread t2 = new Thread(() -> {
synchronized(lock2) {
System.out.println("Thread 2: Holding lock2...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized(lock1) {
System.out.println("Thread 2: Holding lock2 & lock1...");
}
}
});
t1.start();
t2.start();
}
}
What happens:
t1 locks lock1, waits for lock2.
t2 locks lock2, waits for lock1.
Neither can proceed → deadlock.
Lock Ordering: Always acquire multiple locks in the same order.
synchronized(lock1) {
synchronized(lock2) {
// safe
}
}
Try-Lock with Timeout (ReentrantLock): Attempt to acquire lock with a timeout, back off if unavailable.
if(lock.tryLock(1000, TimeUnit.MILLISECONDS)) {
// work
lock.unlock();
}
Avoid Nested Locks: Minimize situations where one thread holds multiple locks.
Use High-Level Concurrency Utilities: java.util.concurrent classes (like ConcurrentHashMap, BlockingQueue) reduce manual locking.