Advanced Java Concurrent Programming —
Complete Masterclass
Threads, Synchronization, Inter-thread Communication
SECTION 1: WHAT IS A THREAD — THE FOUNDATION
The Real Definition (Not the textbook one)
A thread is a lightweight process that shares the same memory space as other threads in the
same process, but has its own execution path and stack.
Why this matters for your exam:
Most students answer: "A thread is a light process."
You need to answer: "A thread is a lightweight process that runs within a process. Multiple
threads share the heap (memory for objects), but each thread has its own stack (method calls,
local variables). This is why threads can modify shared objects but cannot share local
variables."
Single vs Multi-threading
Single-threaded (Sequential):
Main thread executes Task A → Task B → Task C
Execution: Task A completes → Task B starts → Task C starts
Total time = Time(A) + Time(B) + Time(C)
Multi-threaded (Concurrent):
Thread 1: Task A ———→
Thread 2: Task B ———→
Thread 3: Task C ———→
Execution: All run simultaneously
Total time = Max(Time(A), Time(B), Time(C))
Why Java has Threads — The Real Answer
Not just because "it makes things faster." Because:
1. Responsiveness — UI doesn't freeze while downloading data
2. Resource utilization — While one thread waits for I/O, others can compute
3. Fairness — Multiple users/tasks get CPU time
4. Real-world modeling — Real systems have concurrent activities
SECTION 2: THREAD LIFECYCLE — EVERY STATE
EXPLAINED
The 6 States (Not 4 like some textbooks say)
┌──────────────┐
│ NEW │ Thread created but not started
└──────┬───────┘
│ start()
┌──────▼────────────┐
│ RUNNABLE │ Ready to run, waiting for CPU time
└──────┬────────────┘ (Includes both "ready" and "running")
├─ CPU assigns time ──→ Actually executing
└─ waiting for lock ──→ BLOCKED (on monitor lock)
│
└─ wait() called ──→ WAITING (need notify())
└─ sleep() called ──→ TIMED_WAITING (timeout specified)
└─ I/O waiting ──→ BLOCKED
├─ notify() ──────────────┐
│ or timeout ────────┐ │
│ or spurious wake ──┘ │
│ │
└──────┴───────────────────────────┘
Back to RUNNABLE
│ run() completes or exception
┌──▼──────┐
│ TERMINATED
└─────────┘
Each State in Detail
1. NEW
Thread t = new Thread(() -> [Link]("Hello"));
// Thread is created but NOT started yet
// It hasn't entered the runnable queue
// It's just an object in memory
Exam answer: "A thread in NEW state has been instantiated but start() has not been called. No
resources are allocated yet. It is not competing for CPU time."
2. RUNNABLE
[Link]();
// Now the thread is RUNNABLE
// It may be:
// - Actually executing (running on CPU)
// - Waiting for CPU time (ready)
Critical insight: Java has only RUNNABLE state, not separate "READY" and "RUNNING". The
OS manages which RUNNABLE threads actually get CPU.
Why this matters: If you call isAlive() on a RUNNABLE thread, it returns true. But it might not
be executing right now.
3. BLOCKED
synchronized(lock) {
// Thread 1 enters, gets the lock, does work
// Thread 2 tries to enter the synchronized block
// But Thread 1 still holds the lock
// Thread 2 becomes BLOCKED
// It's waiting for the lock to become available
Key difference from WAITING: BLOCKED is waiting for a monitor lock. No notification needed
— it automatically becomes RUNNABLE when the lock is released.
4. WAITING
synchronized(lock) {
[Link](); // Thread calls wait()
// Now it's in WAITING state
// It has released the lock
// It's waiting for notify() or notifyAll()
Critical: wait() MUST be called inside a synchronized block. If you call it outside, you get
IllegalMonitorStateException.
5. TIMED_WAITING
[Link](5000); // TIMED_WAITING for 5 seconds
[Link](5000); // TIMED_WAITING, wakes up after 5 sec or notify()
Difference from WAITING: Has a timeout. Will automatically become RUNNABLE after timeout
even without notification.
6. TERMINATED
public void run() {
[Link]("Working...");
} // Method ends → Thread is TERMINATED
// Once terminated, a thread cannot be restarted
// Calling start() again throws IllegalThreadStateException
State Transitions Code Example
class ThreadLifecycleDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try {
[Link]("Thread in RUNNABLE/RUNNING");
[Link](2000); // TIMED_WAITING
[Link]("After sleep, back to RUNNABLE");
} catch (InterruptedException e) {
[Link]("Thread was interrupted");
});
[Link]("State after creation: " + [Link]()); // NEW
[Link]();
[Link]("State after start(): " + [Link]()); // RUNNABLE
[Link](1000);
[Link]("State during sleep: " + [Link]()); // TIMED_WAITING
[Link](2000);
[Link]("State after completion: " + [Link]()); // TERMINATED
}
}
SECTION 3: CREATING THREADS — THREAD CLASS VS
RUNNABLE
Method 1: Extend Thread Class
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
[Link]([Link]().getName() + ": " + i);
// Usage
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
[Link](); // Starts the thread
[Link]();
}
Method 2: Implement Runnable Interface
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
[Link]([Link]().getName() + ": " + i);
// Usage
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());
Thread t2 = new Thread(new MyRunnable());
[Link]();
[Link]();
Method 3: Lambda Expression (Java 8+)
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
[Link]([Link]().getName() + ": " + i);
}
});
[Link]();
CRITICAL: start() vs run()
// WRONG - This does NOT create a new thread
[Link](); // Calls run() in the MAIN thread, not a new thread
// CORRECT - This creates a new thread
[Link](); // JVM creates new thread, then calls run() in that thread
Why this matters: If you call run() directly, it executes in the current thread. Only start() creates
a new thread and calls run() in it.
Thread Class vs Runnable — Which is Better?
Aspect Extend Thread Implement Runnable
Single Inheritance Can't extend another class Can extend another class
Flexibility Limited More flexible
Reusability Less reusable More reusable
Thread Pool Hard to use Easy to use with
ExecutorService
Best Practice Not recommended Recommended
Exam answer: "Implement Runnable is preferred because Java supports single inheritance. If
you extend Thread, you cannot extend any other class. Also, Runnable is easier to use with
thread pools and ExecutorService."
SECTION 4: THREAD SYNCHRONIZATION
The Problem: Race Condition
class Counter {
private int count = 0;
public void increment() {
count++; // This looks like 1 operation but is actually 3:
// 1. Read current value of count
// 2. Add 1
// 3. Write back to count
// Now two threads call increment() simultaneously
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) [Link]();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) [Link]();
});
[Link]();
[Link]();
[Link]();
[Link]();
[Link]("Expected: 2000, Actual: " + count);
// Output might be: 1847 (not 2000!)
Why?
Timeline of operations (simplified):
Time Thread1 Thread2
1 Read count (0)
2 Read count (0)
3 Add 1, Write (1)
4 Add 1, Write (1)
5 Read count (1)
...
Both threads read 0, both add 1, both write 1.
But we expected 2. This is a race condition.
Solution 1: Synchronized Method
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
What synchronized does:
1. Only ONE thread can execute this method at a time
2. When a thread calls a synchronized method, it acquires the lock (monitor) of the object
3. Other threads wait for the lock to be released
4. When the method exits, the lock is released
Lock is on the OBJECT, not the method:
Counter c1 = new Counter();
Counter c2 = new Counter();
Thread t1 = new Thread(() -> [Link]()); // Locks c1
Thread t2 = new Thread(() -> [Link]()); // Locks c2
// t1 and t2 run simultaneously!
// Each object has its own lock
// They are not competing for the same lock
Solution 2: Synchronized Block
class Counter {
private int count = 0;
private Object lock = new Object();
public void increment() {
synchronized(lock) {
count++;
Advantages over synchronized method:
1. Fine-grained control — Only lock the critical section
2. Multiple locks — Can have different locks for different resources
3. Performance — Non-critical code doesn't hold the lock
Solution 3: More Control with Lock Interface
import [Link].*;
class Counter {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
[Link]();
try {
count++;
} finally {
[Link]();
}
Why try-finally? If an exception occurs, the lock must still be released. try-finally ensures it
happens.
Static Synchronized Methods
class SharedResource {
private static int sharedCounter = 0;
public static synchronized void increment() {
sharedCounter++;
Important: The lock is on the CLASS object ([Link]), not on an instance.
Counter c1 = new Counter();
Counter c2 = new Counter();
Thread t1 = new Thread(() -> [Link]()); // Locks [Link]
Thread t2 = new Thread(() -> [Link]()); // Waits for [Link] lock
// Only ONE thread executes at a time across all instances
Reentrant Lock
class Example {
private Lock lock = new ReentrantLock();
public void method1() {
[Link]();
try {
[Link]("In method1");
method2(); // SAME thread can acquire the SAME lock again
} finally {
[Link]();
public void method2() {
[Link]();
try {
[Link]("In method2");
} finally {
[Link]();
Why "Reentrant"? The same thread can reacquire the same lock multiple times. Without
reentrance, this would deadlock (thread waiting for itself).
SECTION 5: INTER-THREAD COMMUNICATION — wait(),
notify(), notifyAll()
The Problem: Producer-Consumer
class Buffer {
private int value;
private boolean hasValue = false;
// Producer puts data
public void put(int val) {
[Link] = val;
[Link] = true;
// Consumer gets data
public int get() {
if (!hasValue) {
return -1; // No data available (INEFFICIENT)
return value;
}
Problem: Consumer keeps polling (checking hasValue again and again). This wastes CPU. We
need a way for threads to communicate.
The Solution: wait(), notify(), notifyAll()
class Buffer {
private int value;
private boolean hasValue = false;
public synchronized void put(int val) {
while (hasValue) {
try {
wait(); // Producer waits if buffer already has value
} catch (InterruptedException e) {
[Link]().interrupt();
[Link] = val;
[Link] = true;
notify(); // Wake up the consumer
public synchronized int get() {
while (!hasValue) {
try {
wait(); // Consumer waits if no value available
} catch (InterruptedException e) {
[Link]().interrupt();
int val = [Link];
[Link] = false;
notify(); // Wake up the producer
return val;
Why while, not if?
// WRONG:
if (!hasValue) wait();
// CORRECT:
while (!hasValue) wait();
// Reason: After notify(), the thread wakes up but MUST recheck the condition
// Because:
// 1. Another thread might have grabbed the resource before this thread resumes
// 2. Spurious wakeups can happen (thread wakes without explicit notification)
// 3. Multiple threads might be waiting; when notified, you must verify your condition
wait(), notify(), notifyAll() Rules
RULE 1: Must be called from synchronized context
// WRONG - throws IllegalMonitorStateException
void someMethod() {
[Link](); // ERROR!
// CORRECT
synchronized void someMethod() {
[Link](); // OK
// ALSO CORRECT
void someMethod() {
synchronized(lock) {
[Link](); // OK
RULE 2: wait() releases the lock
synchronized void example() {
// At this point, thread holds the lock
wait();
// wait() releases the lock and waits for notification
// Other threads can now enter synchronized methods/blocks
// After notification, thread reacquires the lock before continuing
RULE 3: notify() wakes ONE waiting thread (arbitrary choice)
synchronized void notify() {
notify(); // Wakes exactly ONE waiting thread
// Which one? JVM decides (unpredictable)
// Problem: If you have both producers and consumers waiting,
// notify() might wake a producer when a consumer needed waking
RULE 4: notifyAll() wakes ALL waiting threads
synchronized void example() {
notifyAll(); // Wakes ALL waiting threads
// All compete to reacquire the lock
// They check their conditions and proceed accordingly
// Better for safety: All threads wake and check their conditions
Complete Producer-Consumer Example
class Buffer {
private int[] data = new int[5];
private int count = 0;
private int in = 0; // Insert position
private int out = 0; // Remove position
public synchronized void produce(int value) {
while (count == 5) { // Buffer is full
try {
[Link]("Buffer full, producer waiting...");
wait();
} catch (InterruptedException e) {
[Link]().interrupt();
data[in] = value;
in = (in + 1) % 5;
count++;
[Link]("Produced: " + value + ", Buffer size: " + count);
notifyAll(); // Notify any waiting consumers
}
public synchronized int consume() {
while (count == 0) { // Buffer is empty
try {
[Link]("Buffer empty, consumer waiting...");
wait();
} catch (InterruptedException e) {
[Link]().interrupt();
int value = data[out];
out = (out + 1) % 5;
count--;
[Link]("Consumed: " + value + ", Buffer size: " + count);
notifyAll(); // Notify any waiting producers
return value;
// Usage
public static void main(String[] args) {
Buffer buffer = new Buffer();
Thread producer = new Thread(() -> {
for (int i = 0; i < 10; i++) {
[Link](i);
try { [Link](500); } catch (InterruptedException e) {}
});
Thread consumer = new Thread(() -> {
for (int i = 0; i < 10; i++) {
[Link]();
try { [Link](1000); } catch (InterruptedException e) {}
});
[Link]();
[Link]();
}
SECTION 6: REAL-WORLD PATTERNS
Pattern 1: Thread Pool with ExecutorService
import [Link].*;
public class ThreadPoolExample {
public static void main(String[] args) {
// Create a thread pool with 5 threads
ExecutorService executor = [Link](5);
for (int i = 0; i < 10; i++) {
final int taskId = i;
[Link](() -> {
[Link]("Task " + taskId + " running in " +
[Link]().getName());
try { [Link](2000); } catch (InterruptedException e) {}
});
[Link](); // No new tasks accepted
[Link](10, [Link]); // Wait for completion
}
Pattern 2: Volatile for Simple Flags
class VolatileExample {
private volatile boolean flag = false; // Changes are visible across threads
public void setFlag(boolean value) {
flag = value;
public void waitForFlag() {
while (!flag) {
[Link](); // Efficient waiting for flag
// When to use volatile instead of synchronized:
// - Single variable
// - Only reads and writes (no compound operations)
// - Performance-critical code
Pattern 3: CountDownLatch
import [Link].*;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int taskCount = 5;
CountDownLatch latch = new CountDownLatch(taskCount);
for (int i = 0; i < taskCount; i++) {
new Thread(() -> {
[Link]("Task running...");
try { [Link](1000); } catch (InterruptedException e) {}
[Link](); // Signal completion
}).start();
[Link](); // Main thread waits for all tasks to complete
[Link]("All tasks completed!");
SECTION 7: EXAM QUESTIONS — ANSWERED AT DEPTH
Q1: What is the difference between Thread class and Runnable interface?
Surface answer: "Thread class extends Thread, Runnable implements Runnable."
Depth answer: "Thread class and Runnable interface are two ways to create threads:
1. Inheritance vs Interface:
- Thread: Extends Thread class (single inheritance limitation)
- Runnable: Implements Runnable interface (can extend another class)
2. Flexibility:
- Thread: If you need to extend another class, you can't use Thread
- Runnable: More flexible, recommended for new code
3. Usage with Thread Pools:
- Thread: Hard to use with ExecutorService
- Runnable: Designed for use with thread pools
4. Best Practice:
- Runnable is the recommended approach because of Java's single inheritance
constraint. Even though Thread implements Runnable, the industry standard is to
implement Runnable directly.
Example:
// Limited - can't extend another class
class MyThread extends Thread { }
// Flexible - can extend any class
class MyRunnable extends SomeOtherClass implements Runnable { }
5. Performance:
- No meaningful difference
- The real difference is architectural"
Q2: Explain the lifecycle of a thread with all states.
Surface answer: "A thread has 4 states: new, runnable, blocked, terminated."
Depth answer: "A thread has 6 states in Java:
1. NEW: Created but not started
- Thread t = new Thread(...) → thread is NEW
- No resources allocated
- getState() returns [Link]
2. RUNNABLE: Started and ready to run
- [Link]() → thread becomes RUNNABLE
- Includes both 'ready to run' and 'actually running'
- Java doesn't distinguish between them
- CPU scheduler decides when it actually gets CPU time
3. BLOCKED: Waiting for monitor lock
- When thread tries to enter synchronized block/method but lock is held
- getState() returns [Link]
- Automatically becomes RUNNABLE when lock is released
4. WAITING: Waiting for notification
- [Link]() → thread enters WAITING
- Thread has released its lock
- Waits indefinitely for notify() or notifyAll()
- getState() returns [Link]
5. TIMED_WAITING: Waiting with timeout
- [Link](ms) → TIMED_WAITING
- [Link](ms) → TIMED_WAITING
- Will automatically return to RUNNABLE after timeout
- OR when notify() is called (whichever comes first)
6. TERMINATED: Execution completed
- run() method completed or threw exception
- Thread is dead, cannot be restarted
- Calling start() again throws IllegalThreadStateException
State transitions:
NEW → start() → RUNNABLE
(CPU time)
↓
↙wait() ↓ lock unavailable↘
WAITING RUNNABLE BLOCKED
↘ ↗ notify()↗
RUNNABLE
run() ends
TERMINATED
Key insight: The JVM schedules RUNNABLE threads. We can't control which RUNNABLE
thread gets CPU time. That's the OS scheduler's job."
Q3: What happens when you call wait() without synchronized?
Surface answer: "It throws an error."
Depth answer: "Calling wait() outside synchronized context throws
IllegalMonitorStateException.
Why?
Wait() is designed to work with the monitor lock system:
1. When you call wait(), the thread releases the lock it holds
2. Without synchronized context, there is NO lock to release
3. The JVM cannot enforce the lock/release mechanism
4. Therefore, it throws IllegalMonitorStateException
Example:
// WRONG:
void wrongMethod() {
[Link](); // IllegalMonitorStateException
// CORRECT:
synchronized void rightMethod() {
[Link](); // OK
// ALSO CORRECT:
void anotherRight() {
synchronized(lock) {
[Link](); // OK
Why this design?
Wait() needs a lock for three reasons:
1. Atomicity: Release lock and wait must be atomic (one operation)
2. Condition checking: You must hold the lock while checking the condition and calling
wait()
3. Wake-up safety: notify() requires the waiting thread to reacquire the same lock
If wait() could be called without synchronized, these guarantees would break."
Q4: What is the difference between notify() and notifyAll()?
Surface answer: "notify() wakes one thread, notifyAll() wakes all."
Depth answer: "Both wake waiting threads, but in different ways:
notify():
synchronized void produce() {
// ... produce item ...
notify(); // Wakes exactly ONE arbitrary waiting thread
- Wakes one arbitrary waiting thread (JVM chooses which one)
- More efficient (less context switching)
- Problem: If both producers and consumers are waiting, notify() might wake the wrong
type
- Use when: Exactly one thread should be woken
notifyAll():
synchronized void produce() {
// ... produce item ...
notifyAll(); // Wakes ALL waiting threads
- Wakes every thread waiting on this object
- All threads reacquire the lock and proceed
- They check their conditions; if not met, they wait again
- Safer: All threads get a chance to check their condition
- Use when: You're unsure which thread should wake or multiple threads need waking
Example of problem with notify():
class ProducerConsumer {
private int value;
private boolean full = false;
synchronized void produce(int val) {
while (full) wait();
value = val;
full = true;
notify(); // PROBLEM: Might wake another producer!
// If another producer wakes, it will wait() again
synchronized int consume() {
while (!full) wait();
int val = value;
full = false;
notify(); // PROBLEM: Might wake another consumer!
// If another consumer wakes, it will wait() again
// If only one producer and one consumer: notify() works fine
// If multiple producers/consumers: notifyAll() is safer
Best Practice: Use notifyAll() unless you have a specific reason to use notify(). The
performance difference is negligible, but safety is paramount in concurrent programming."
Q5: Why use while instead of if with wait()?
Surface answer: "Because the condition might change."
Depth answer: "You MUST use while, not if, when calling wait():
// WRONG:
synchronized void wrong() {
if (!condition) wait();
// Use resource
// CORRECT:
synchronized void right() {
while (!condition) wait();
// Use resource
Why?
Three reasons:
1. Spurious Wakeups: A thread can wake from wait() without explicit notification. This is
rare but legal:
synchronized void spuriousWakeup() {
while (!condition) wait(); // If woken spuriously, loop checks condition again
// By the time we get here, condition is definitely true
2. Multiple Waiting Threads: When notifyAll() is called, all threads wake:
synchronized void buggyVersion() {
if (!condition) wait();
// Thread 1 wakes, uses resource
// Thread 2 wakes, BUT CONDITION IS NOW FALSE
// It proceeds anyway - BUG!
synchronized void fixedVersion() {
while (!condition) wait();
// Thread 1 wakes, uses resource, sets condition = false
// Thread 2 wakes, checks condition (false), waits again
// Correct behavior
3. Lost Notification: Race condition between notify and wait:
// Timeline:
T1: Check condition (true) → about to wait()
T2: Change condition to false
T2: notify() // T1 hasn't called wait() yet!
T1: wait() // Wait for notification that already happened - DEADLOCK!
// With while:
T1: while (condition) wait();
T1: Check condition (true)
T2: Change condition to false
T2: notify()
T1: wait() // Wakes immediately (was already true before wait)
T1: Check condition (false), exit loop - CORRECT!
Rule: Always use while(condition) wait(), never if(condition) wait()."
FINAL EXAM CHECKLIST
Before the exam, make sure you can answer these at depth:
What is a thread vs process?
All 6 thread states and transitions
Why start() not run()
Thread vs Runnable with specific examples
Race condition and why it happens
How synchronized works (lock acquired/released)
Difference between synchronized method and block
Why wait() needs synchronized context
while vs if with wait()
notify() vs notifyAll() with examples
Producer-consumer pattern end-to-end
Reentrant locks and why "reentrant"
Real-world patterns (thread pools, executors)
REMEMBER
You are not just trying to pass the exam. You are trying to answer so deeply that the examiner
realizes you understand not just WHAT threads do, but WHY they work that way.
Every answer should be:
1. Technically correct
2. Explain the WHY (not just what)
3. Include code examples
4. Mention edge cases or common mistakes
5. Connect to real-world usage
This depth will separate your answers from every other student in that room.
You've got this. Now go master it.