Thread Communication in Java: join, wait/notify, CountDownLatch, CyclicBarrier, Callable and FutureTask
This article explains how to achieve thread communication and coordination in Java using Thread.join(), Object.wait()/notify(), CountDownLatch, CyclicBarrier, and the Callable/FutureTask pair, providing complete code examples and execution results for each technique.
In Java, thread communication can be achieved using various mechanisms such as Thread.join() , Object.wait() / Object.notify() , CountDownLatch , CyclicBarrier , and the Callable / FutureTask pair.
Sequential execution of two threads – By calling Thread.join() on the first thread, the second thread waits until the first finishes. Example code:
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 the above prints interleaved output because the threads run concurrently.
To make B start only after A finishes, join() is used:
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 开始等待 A");
try{ A.join(); }catch(InterruptedException e){ e.printStackTrace(); }
printNumber("B");
}
});
B.start();
A.start();
}The output shows that B waits until A completes.
Ordered interleaving of two threads – When finer‑grained ordering is required, wait() and notify() on a shared lock object can be used:
/** A 1, B 1, B 2, B 3, A 2, A 3 */
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 result is the desired sequence: A prints 1, then B prints 1‑3, then A prints 2‑3.
Using CountDownLatch for "wait for multiple threads" – When several worker threads must finish before a dependent thread proceeds, a CountDownLatch can be used. Example:
private static void runDAfterABC(){
int worker = 3;
CountDownLatch countDownLatch = new CountDownLatch(worker);
new Thread(() -> {
System.out.println("D is waiting for other three threads");
try{ countDownLatch.await();
System.out.println("All done, D starts working");
}catch(InterruptedException e){ e.printStackTrace(); }
}).start();
for(char threadName='A'; threadName<='C'; threadName++){
final String tN = String.valueOf(threadName);
new Thread(() -> {
System.out.println(tN + "is working");
try{ Thread.sleep(100); }catch(Exception e){ e.printStackTrace(); }
System.out.println(tN + "finished");
countDownLatch.countDown();
}).start();
}
}The latch ensures that thread D starts only after A, B and C have called countDown() .
Using CyclicBarrier for simultaneous start after preparation – When a group of threads must wait for each other before proceeding, CyclicBarrier is appropriate:
private static void runABCWhenAllReady(){
int runner = 3;
CyclicBarrier cyclicBarrier = new CyclicBarrier(runner);
Random random = new Random();
for(char runnerName='A'; runnerName<='C'; runnerName++){
final String rN = String.valueOf(runnerName);
new Thread(() -> {
long prepareTime = random.nextInt(10000) + 100;
System.out.println(rN + "is preparing for time:" + prepareTime);
try{ Thread.sleep(prepareTime); }catch(Exception e){ e.printStackTrace(); }
System.out.println(rN + "is prepared, waiting for others");
try{ cyclicBarrier.await(); }catch(InterruptedException|BrokenBarrierException e){ e.printStackTrace(); }
System.out.println(rN + "starts running");
}).start();
}
}All three threads print their “ready” message, then they start running together.
Returning a result from a worker thread – The Callable interface allows a thread to produce a value. Wrapped in a FutureTask , the main thread can retrieve the result with get() (which blocks until the computation finishes):
private static void doTaskWithResultInWorker(){
Callable
callable = new Callable
(){
@Override public Integer call() throws Exception{
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(InterruptedException|ExecutionException e){ e.printStackTrace(); }
}The output shows the main thread blocked on futureTask.get() , then receiving the computed sum (5050) once the worker finishes.
Overall, the article demonstrates how to coordinate Java threads using basic joins, low‑level wait/notify, higher‑level synchronization utilities, and how to retrieve computation results from background threads.
Java Captain
Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.
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.