Java Thread Communication Techniques: join, wait/notify, CountDownLatch, CyclicBarrier, Callable and FutureTask
This article explains various Java thread‑communication mechanisms—including thread.join(), object.wait()/notify(), CountDownLatch, CyclicBarrier, Callable, and FutureTask—through clear examples, code snippets, and execution results, helping developers coordinate thread execution and retrieve results from worker threads.
In normal execution each thread finishes its own task independently, but sometimes multiple threads need to cooperate, which involves thread communication. The following concepts are covered:
thread.join()
object.wait() and object.notify()
CountDownLatch
CyclicBarrier
FutureTask
Callable
How to make two threads execute sequentially?
Two threads A and B print numbers 1‑3 concurrently. The code demonstrates the default interleaved output and then shows how using thread.join() forces B to wait until A finishes.
private static void demo1() {
Thread A = new Thread(new Runnable() {
@Override
public void run() {
printNumber("A");
}
});
Thread B = new Thread(new Runnable() {
@Override
public void run() {
printNumber("B");
}
});
A.start();
B.start();
}
private static void printNumber(String threadName) {
int i = 0;
while (i++ < 3) {
try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(threadName + " print: " + i);
}
}Running this yields interleaved output. By inserting A.join(); in B’s run method, B waits for A to complete before printing.
private static void demo2() {
Thread A = new Thread(new Runnable() {
@Override
public void run() { printNumber("A"); }
});
Thread B = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("B starts waiting for A");
try { A.join(); } catch (InterruptedException e) { e.printStackTrace(); }
printNumber("B");
}
});
B.start();
A.start();
}How to make two threads interleave their execution?
Using object.wait() and object.notify() we can coordinate finer‑grained ordering. Thread A prints its first value, then calls lock.wait() to release the lock. Thread B acquires the lock, prints all its values, and calls lock.notify() to wake A.
private static void demo3() {
Object lock = new Object();
Thread A = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
System.out.println("A 1");
try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("A 2");
System.out.println("A 3");
}
}
});
Thread B = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
System.out.println("B 1");
System.out.println("B 2");
System.out.println("B 3");
lock.notify();
}
}
});
A.start();
B.start();
}The output matches the expected order: A 1 → B 1 2 3 → A 2 3.
Waiting for multiple threads before proceeding (CountDownLatch)
When thread D must start only after threads A, B, and C have all finished, CountDownLatch provides a simple solution. D calls await() , while each of A‑C calls countDown() when done.
private static void runDAfterABC() {
int worker = 3;
CountDownLatch latch = new CountDownLatch(worker);
new Thread(() -> {
System.out.println("D is waiting for other three threads");
try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("All done, D starts working");
}).start();
for (char name = 'A'; name <= 'C'; name++) {
final String t = String.valueOf(name);
new Thread(() -> {
System.out.println(t + " is working");
try { Thread.sleep(100); } catch (Exception e) { e.printStackTrace(); }
System.out.println(t + " finished");
latch.countDown();
}).start();
}
}The latch ensures D runs only after A, B, and C have completed.
Coordinating a group of threads to start together (CyclicBarrier)
To model three athletes preparing independently and then running together, CyclicBarrier is used. Each thread calls await() after preparation; when all have arrived, they proceed simultaneously.
private static void runABCWhenAllReady() {
int runner = 3;
CyclicBarrier barrier = new CyclicBarrier(runner);
Random random = new Random();
for (char name = 'A'; name <= 'C'; name++) {
final String r = String.valueOf(name);
new Thread(() -> {
long prepare = random.nextInt(10000) + 100;
System.out.println(r + " is preparing for time: " + prepare);
try { Thread.sleep(prepare); } catch (Exception e) { e.printStackTrace(); }
System.out.println(r + " is prepared, waiting for others");
try { barrier.await(); } catch (Exception e) { e.printStackTrace(); }
System.out.println(r + " starts running");
}).start();
}
}Returning results from a worker thread (Callable, FutureTask)
Unlike Runnable , Callable can return a value. Combined with FutureTask , the main thread can retrieve the result, though get() blocks until the computation finishes.
private static void doTaskWithResultInWorker() {
Callable
callable = () -> {
System.out.println("Task starts");
Thread.sleep(1000);
int result = 0;
for (int i = 0; i <= 100; i++) { result += i; }
System.out.println("Task finished and return result");
return result;
};
FutureTask
futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
try {
System.out.println("Before futureTask.get()");
System.out.println("Result: " + futureTask.get());
System.out.println("After futureTask.get()");
} catch (Exception e) { e.printStackTrace(); }
}The example prints the sum of numbers 1‑100 (5050) after the worker thread completes.
Conclusion
Thread communication, synchronization, and safety are essential topics in modern programming languages. This article covered Java’s core communication tools— join , wait/notify , CountDownLatch , CyclicBarrier , Callable , and FutureTask —and laid the groundwork for deeper exploration of thread synchronization and safety.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.