Asynchrony and Reactivity in Java: CompletableFuture, Flow, and Virtual Threads

Async and Reactivity in Java: CompletableFuture, Flow, and Virtual Threads

In modern Java development, there are three main approaches to asynchrony and concurrency:

  • CompletableFuture — for single asynchronous tasks.
  • Flow / Reactive Streams — for data flows with backpressure.
  • Virtual Threads / Loom — for scalable, lock-free concurrency.

Figurative Understanding

Flow is a "river of data with flow control."
Virtual Threads are "millions of workers" ready to process data at their own speed, but unable to slow the river.
CompletableFuture is a "single load" delivered asynchronously.

Comparison of Approaches

Mechanism Strength When to Use
CompletableFuture (Java 8) Simple Asynchrony for Single Tasks, Action Chains API Requests, DB, File Operations
Flow / Reactive Streams (Java 9) Backpressure Data Flow, Event Processing Pipelines Streaming, Message Brokers, WebFlux, Event-Driven Systems
Virtual Threads / Loom (Java 21) Massively parallel, lock-free, linear code Web servers, APIs, scalable services

Code examples

1. CompletableFuture — a single asynchronous task


import java.util.concurrent.*;

public class CompletableFutureExample {

    public static void main(String[] args) throws Exception {

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            sleep(500);
            return "Hello from CompletableFuture";
        });

        future.thenAccept(System.out::println);

        Thread.sleep(1000);
    }

    private static void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {}
    }
}

2. Flow - data flow with backpressure


import java.util.concurrent.Flow.*;
import java.util.concurrent.SubmissionPublisher;

public class FlowExample {

    public static void main(String[] args) throws Exception {

        SubmissionPublisher<Integer> publisher = new SubmissionPublisher<>();

        Subscriber<Integer> subscriber = new Subscriber<>() {

            private Subscription subscription;

            @Override
            public void onSubscribe(Subscription subscription) {
                this.subscription = subscription;
                subscription.request(5);
            }

            @Override
            public void onNext(Integer item) {
                System.out.println("Received: " + item);
                sleep(200);
            }

            @Override
            public void onError(Throwable throwable) {
                throwable.printStackTrace();
            }

            @Override
            public void onComplete() {
                System.out.println("Done!");
            }
        };

        publisher.subscribe(subscriber);

        for (int i = 1; i <= 10; i++) {
            publisher.submit(i);
        }

        publisher.close();
        Thread.sleep(3000);
    }

    private static void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {}
    }
}

3. Virtual Threads - millions of parallel tasks (Java 21+)


public class VirtualThreadsExample {

    public static void main(String[] args) throws Exception {

        for (int i = 1; i <= 10; i++) {

            Thread.startVirtualThread(() -> {
                System.out.println("Hello from virtual thread " + Thread.currentThread().getName());
                sleep(200);
            });

        }

        Thread.sleep(1000);
    }

    private static void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {}
    }
}
 
Flow (data river)
[Publisher] --> [Subscriber] --> [Subscriber]
^ speed control (backpressure)

Virtual Threads (workers)
[Task1] [Task2] [Task3] ... [TaskN]
each one works at its own speed, no one slows down

CompletableFuture — single payload
Async Task ---> Result
\
---> thenAccept / thenApply

When callbacks are needed


CompletableFuture -> callbacks almost always
Flow -> callbacks via onNext/onComplete
Virtual Threads -> callbacks almost never needed

Conclusion

Each asynchronous model has its own strengths and is used for different business tasks:

  • CompletableFuture - for single tasks where simplicity is important.
  • Flow - for rate-controlled data flows where reliability and backpressure are important.
  • Virtual Threads - for scalable servers where readability and parallelism without callbacks are important.

🌐 На русском
Total Likes:0
My social media channel
By sending an email, you agree to the terms of the privacy policy

Useful Articles:

Современный подход к параллелизму в Java - Fork/Join Framework, CompletableFuture и виртуальные потоки (Project Loom)
```html id="b6v9kc" ``` Virtual threads are especially useful for high-volume I/O operations—for example, when processing HTTP requests or accessing external APIs. The code remains linear and readabl...
Asynchrony in Java: Future, CompletableFuture, and Structured Concurrency
Java was originally designed for multithreading and parallel computing. Over time, various methods for working with the results of asynchronous tasks have emerged—from the classic Future to modern Str...
Understanding Multithreading in Java Through Collections and Atomics
1️⃣ HashMap / TreeMap / TreeSet (not thread-safe) HashMap: Structure: array of buckets + linked lists / trees (for collisions). Under the hood: put/remove modifies the bucket array and possibly reord...

New Articles:

Generics, Reflection and Channels - Go vs Java | Types - Language
In this article we will analyze advanced type system features in Go: generics (type parameters), reflection, and channel types for concurrency. We will compare Go and Java approaches, so Java develope...
Let's look at: Trace, Profiling, Integration Testing, Code Coverage, Mocking, Deadlock Detection in Go vs Java | Testing, Debugging and Profiling
Series: Go for Java Developers — analysis of trace, profiling and testing In this article we will analyze tools and practices for testing, debugging and profiling in Go. For a Java developer this wil...
Let's Break It Down: Rate Limiter, Non-Blocking Operations, and Scheduler: Go vs. Java | Concurrency Part 4
This article is dedicated to understanding the principles of concurrency and synchronization in Go and Java. We ll cover key approaches such as rate-limiter, non-blocking operations, and task scheduli...
Fullscreen image