🔄 Controlling the execution order of threads (T1 → T2 → T3) is a classic multithreading problem, and there are several ways to achieve it — depending on your style and needs.
✅ Goal: Run Threads in This Exact Order
T1 prints → T2 prints → T3 prints
🔧 Solution 1: Using join() — Simple & Clean
public class OrderedThreads {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> System.out.println("T1"));
Thread t2 = new Thread(() -> System.out.println("T2"));
Thread t3 = new Thread(() -> System.out.println("T3"));
t1.start();
t1.join(); // Main thread waits for T1
t2.start();
t2.join(); // Main thread waits for T2
t3.start();
t3.join(); // Main thread waits for T3
}
}
✅ Simple and ensures exact order
🔧 Solution 2: Using CountDownLatch — Thread Coordination
public class OrderedThreads {
public static void main(String[] args) {
CountDownLatch latch1 = new CountDownLatch(1);
CountDownLatch latch2 = new CountDownLatch(1);
Thread t1 = new Thread(() -> {
System.out.println("T1");
latch1.countDown(); // Let T2 proceed
});
Thread t2 = new Thread(() -> {
try {
latch1.await(); // Wait for T1
System.out.println("T2");
latch2.countDown(); // Let T3 proceed
} catch (InterruptedException e) { e.printStackTrace(); }
});
Thread t3 = new Thread(() -> {
try {
latch2.await(); // Wait for T2
System.out.println("T3");
} catch (InterruptedException e) { e.printStackTrace(); }
});
t1.start();
t2.start();
t3.start();
}
}
✅ Scalable for more complex workflows
🔧 Solution 3: Using synchronized + wait()/notify()
class OrderedPrinter {
private int state = 1;
public synchronized void printT1() throws InterruptedException {
while (state != 1) wait();
System.out.println("T1");
state = 2;
notifyAll();
}
public synchronized void printT2() throws InterruptedException {
while (state != 2) wait();
System.out.println("T2");
state = 3;
notifyAll();
}
public synchronized void printT3() throws InterruptedException {
while (state != 3) wait();
System.out.println("T3");
}
}
public class OrderedThreads {
public static void main(String[] args) {
OrderedPrinter printer = new OrderedPrinter();
Thread t1 = new Thread(() -> {
try { printer.printT1(); } catch (Exception e) {}
});
Thread t2 = new Thread(() -> {
try { printer.printT2(); } catch (Exception e) {}
});
Thread t3 = new Thread(() -> {
try { printer.printT3(); } catch (Exception e) {}
});
t2.start();
t3.start();
t1.start(); // Can be started out of order — logic ensures sequence
}
}
✅ Works even if threads start out of order ✅ Good for repeated sequences (looped coordination)
✅ Which One to Use?
| Method | Best For | Pros | Cons |
|---|---|---|---|
join() | Simple one-time sequencing | Easy, direct | Main thread must coordinate |
CountDownLatch | Multiple threads / flow control | Clear intent, reusable | Slightly more boilerplate |
wait()/notify | Repeating patterns / flexibility | Works out-of-order start | Needs careful handling of wait-notify |
Should not be state volatile here ?
🔍 Should state Be volatile in the wait()/notify() Solution?
✅ No, it doesn’t have to be volatile — and here’s why:
When you’re using synchronized + wait()/notify(), the synchronized block already guarantees:
- Mutual exclusion (only one thread enters at a time)
- Visibility — changes made by one thread are visible to others that acquire the same lock
💡
synchronizedensures memory visibility, just likevolatile.
📘 JLS (Java Language Spec) Guarantee:
“Entering a synchronized block has the same happens-before semantics as reading a
volatilevariable.”
✅ So in this code:
private int state = 1;
public synchronized void printT1() throws InterruptedException {
while (state != 1) wait();
System.out.println("T1");
state = 2;
notifyAll();
}
- Thread A updates
stateinsidesynchronized - Thread B, also inside a
synchronizedmethod, readsstate - ✅ This works safely without
volatile
❗ When would you need volatile?
Use volatile when:
- You’re not using
synchronized - And you want visibility only, not atomicity
Example:
private volatile boolean running = true;
while (running) {
// do something
}
Here, running must be volatile because there’s no synchronization.
🧠 Summary
| Context | Need volatile? |
|---|---|
Inside synchronized block | ❌ No |
| Accessed without locks | ✅ Yes |
Used with wait()/notify() | ❌ No (lock gives visibility) |