Backend Development 11 min read

Converting Asynchronous Calls to Synchronous in Java: Five Common Techniques

This article explains how to turn Java asynchronous invocations into synchronous ones by demonstrating five approaches—wait/notify, condition locks, Future, CountDownLatch, and CyclicBarrier—each illustrated with complete code examples and detailed execution flow.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Converting Asynchronous Calls to Synchronous in Java: Five Common Techniques

Sunny first clarifies the difference between synchronous and asynchronous calls: a synchronous call blocks the caller until a result is returned, while an asynchronous call returns immediately and usually delivers the result via a callback.

The article then focuses on converting asynchronous Java calls to synchronous ones by blocking the calling thread until the result arrives. Five methods are presented.

0. Constructing an Asynchronous Call

A simple asynchronous model is built with an AsyncCall class that spawns a thread, sleeps for a random duration, and invokes demo.callback(res) . The BaseDemo abstract class holds an AsyncCall instance and defines an abstract callback method that concrete demos must implement.

public class AsyncCall {
    private Random random = new Random(System.currentTimeMillis());
    private ExecutorService tp = Executors.newSingleThreadExecutor();
    // demo1,2,4,5 call method
    public void call(BaseDemo demo) {
        new Thread(() -> {
            long res = random.nextInt(10);
            try { Thread.sleep(res * 1000); } catch (InterruptedException e) { e.printStackTrace(); }
            demo.callback(res);
        }).start();
    }
    // demo3 call method
    public Future
futureCall() {
        return tp.submit(() -> {
            long res = random.nextInt(10);
            try { Thread.sleep(res * 1000); } catch (InterruptedException e) { e.printStackTrace(); }
            return res;
        });
    }
    public void shutdown() { tp.shutdown(); }
}
public abstract class BaseDemo {
    protected AsyncCall asyncCall = new AsyncCall();
    public abstract void callback(long response);
    public void call() {
        System.out.println("发起调用");
        asyncCall.call(this);
        System.out.println("调用返回");
    }
}

1. Using wait and notify

The first technique employs the intrinsic lock of an object. The main thread calls demo1.lock.wait() after invoking demo1.call() , and the callback invokes lock.notifyAll() to wake the waiting thread.

public class Demo1 extends BaseDemo {
    private final Object lock = new Object();
    @Override
    public void callback(long response) {
        System.out.println("得到结果");
        System.out.println(response);
        System.out.println("调用结束");
        synchronized (lock) { lock.notifyAll(); }
    }
    public static void main(String[] args) {
        Demo1 demo1 = new Demo1();
        demo1.call();
        synchronized (demo1.lock) {
            try { demo1.lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); }
        }
        System.out.println("主线程内容");
    }
}

2. Using a Condition Lock

The second method replaces the intrinsic lock with a ReentrantLock and a Condition . The main thread awaits the condition, while the callback signals it.

public class Demo2 extends BaseDemo {
    private final Lock lock = new ReentrantLock();
    private final Condition con = lock.newCondition();
    @Override
    public void callback(long response) {
        System.out.println("得到结果");
        System.out.println(response);
        System.out.println("调用结束");
        lock.lock();
        try { con.signal(); } finally { lock.unlock(); }
    }
    public static void main(String[] args) {
        Demo2 demo2 = new Demo2();
        demo2.call();
        demo2.lock.lock();
        try { demo2.con.await(); } catch (InterruptedException e) { e.printStackTrace(); } finally { demo2.lock.unlock(); }
        System.out.println("主线程内容");
    }
}

3. Using Future

The third approach leverages Future . The asynchronous method returns a Future<Long> from a thread‑pool task; the main thread polls isDone() and then retrieves the result with get() .

public class Demo3 {
    private AsyncCall asyncCall = new AsyncCall();
    public Future
call() {
        Future
future = asyncCall.futureCall();
        asyncCall.shutdown();
        return future;
    }
    public static void main(String[] args) {
        Demo3 demo3 = new Demo3();
        System.out.println("发起调用");
        Future
future = demo3.call();
        System.out.println("返回结果");
        while (!future.isDone() && !future.isCancelled());
        try { System.out.println(future.get()); } catch (Exception e) { e.printStackTrace(); }
        System.out.println("主线程内容");
    }
}

4. Using CountDownLatch

The fourth technique uses a CountDownLatch . The main thread calls await() , and the callback invokes countDown() to release the latch.

public class Demo4 extends BaseDemo {
    private final CountDownLatch countDownLatch = new CountDownLatch(1);
    @Override
    public void callback(long response) {
        System.out.println("得到结果");
        System.out.println(response);
        System.out.println("调用结束");
        countDownLatch.countDown();
    }
    public static void main(String[] args) {
        Demo4 demo4 = new Demo4();
        demo4.call();
        try { demo4.countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("主线程内容");
    }
}

5. Using CyclicBarrier

The final method employs a CyclicBarrier initialized with 2 parties (the async thread and the main thread). Both threads call await() and proceed once both have arrived.

public class Demo5 extends BaseDemo {
    private CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
    @Override
    public void callback(long response) {
        System.out.println("得到结果");
        System.out.println(response);
        System.out.println("调用结束");
        try { cyclicBarrier.await(); } catch (Exception e) { e.printStackTrace(); }
    }
    public static void main(String[] args) {
        Demo5 demo5 = new Demo5();
        demo5.call();
        try { demo5.cyclicBarrier.await(); } catch (Exception e) { e.printStackTrace(); }
        System.out.println("主线程内容");
    }
}

All five methods share the same core idea: the calling thread blocks until the asynchronous task signals completion, either through wait/notify, condition variables, Future polling, latch countdown, or barrier synchronization.

JavaasynchronousSynchronizationFutureCountDownLatchCyclicBarrierwait-notify
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

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.