Fundamentals 14 min read

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.

Java Captain
Java Captain
Java Captain
Thread Communication in Java: join, wait/notify, CountDownLatch, CyclicBarrier, Callable and FutureTask

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.

JavaConcurrencyThreadSynchronizationCallableCountDownLatchCyclicBarrierFutureTask
Java Captain
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.