- Preface
- Comparison of Approaches to Parallelism in Java
- 1️⃣ Fork/Join Framework
- Key elements:
- Example:
- 2️⃣ CompletableFuture — asynchronous programming with functional style
- Main features:
- Example of a CompletableFuture chain:
- 3️⃣ Virtual Threads (Project Loom)
- Advantages:
- Example of Virtual Threads:
- 4️⃣ How to Choose the Right Tool
- Transition from Threads to Modern Tools
- On what basis CompletableFuture makes decisions
- Virtual Threads (Project Loom) — The Future of Multithreading in Java
- In Brief
- Example: Regular Threads vs Virtual Threads
- Key Advantages
- When to Use
- Limitations and Pitfalls
- Practical Migration
- Summary
- Virtual Streams Loom as a Metaverse
- Comparison of Approaches to Parallelism in Java and Prerequisites for Choosing
- Conclusion
Modern approach to parallelism in Java - Fork/Join Framework, CompletableFuture, and virtual threads (Project Loom)
Preface
The world of software has long ceased to be a calm ocean: today it is a turbulent ecosystem where every millisecond of application response can cost a company customers, reputation, or money. Modern business systems — online stores, banking platforms, analytical services, social networks — operate under conditions of load, scale, and constant expectation of instant response. If a page hangs for a second longer, the user leaves; if a request to a database or external API blocks the flow, the business loses profit.
Every delay in the code is not just a millisecond. It’s a lost order, customer, trust.
Previously, companies simply added more servers and threads to handle the influx of customers. But this strategy quickly showed its limits. Each thread in the operating system consumes memory and resources. Creating thousands of threads for each request results in a significant portion of the CPU being spent not on useful work, but on context switching and waiting for input/output. This gave rise to the classic symptoms of overloaded systems: the server processes only a part of the requests, the task queue grows, and users see timeout or 503 Service Unavailable errors.
When the infrastructure is busy waiting rather than computing — the business pays for downtime.
For businesses, this means direct losses: unclosed orders, failures in integrations, decreased customer trust. IT departments respond: “we need to optimize parallelism.” But what does this mean in practice?
Modern Java offers three generations of tools that evolutionarily solve the same problem — how to make the machine work simultaneously on multiple tasks without overheating. First came the Fork/Join Framework, which allowed complex computational tasks to be efficiently divided among the processor cores. Then CompletableFuture introduced asynchronous, reactive thinking — where the system does not wait for a result but continues working. And finally, virtual threads from Project Loom changed the very philosophy: now you can write familiar linear code, but the JVM itself transforms it into a scalable non-blocking architecture.
From Fork/Join to Loom — it’s not just the evolution of technologies. It’s the path for businesses to efficiency, resilience, and speed.
This article is not about syntax or beautiful APIs. It is about how parallelism technologies solve real business problems — from optimizing server computations and speeding up responses to reducing infrastructure costs. Understanding these tools becomes not just technical knowledge, but a strategic advantage for developers and companies striving to be fast, resilient, and ready for growth.
Modern applications require high scalability and responsiveness. With the increase in the number of processor cores, the task of effective parallelism has become one of the key challenges. Java provides several tools for organizing parallel and asynchronous execution of tasks — from Fork/Join Framework and CompletableFuture to virtual threads from Project Loom. Let's break them down in detail to understand where each is most effective.
Comparison of Approaches to Parallelism in Java
Different mechanisms for parallel execution in Java appeared at different times and solve different tasks. Below is a summary table that helps to see where each tool is effective and what limitations it has.
| Characteristic | Regular Threads (Thread) |
Fork/Join Framework |
CompletableFuture |
Virtual Threads (Project Loom) |
|---|---|---|---|---|
| Year of Appearance | Java 1.0 | Java 7 | Java 8 | Java 21 |
| Main Idea | Each task is a separate thread | Divide-and-conquer: breaking tasks into subtasks | Asynchronous chains and task composition | Millions of lightweight threads managed by the JVM |
| Type of Tasks | Simple, small, low parallelism | CPU-bound (lots of computations) | I/O-bound (requests, network calls) | Massive I/O, scalable services |
| Thread Creation | Manual (new Thread()) |
Automatically from the pool (ForkJoinPool) |
Asynchronously, via a pool (commonPool or Executor) |
Automatically, millions of virtual threads |
| Scheduling | The OS manages each thread | JVM with Work-Stealing | JVM through ForkJoinPool or Executor |
JVM manages scheduling at the user code level |
| Memory per Thread | ~1 MB | ~1 MB | ~1 MB (depends on the pool) | ~2–3 KB (dynamic) |
| Code Simplicity | Simple, but quickly becomes cumbersome | More complex, requires recursive thinking | Concise, declarative | Very simple: looks like synchronous code |
| Debugging Difficulty | Low | High with recursion | Medium (call chains) | Low — standard call stack |
| Support for Blocking Calls | Blocks the thread | Not recommended | Not recommended | Supported without blocking the core |
| Scalability | Poor (limited by OS resources) | Good for CPU-bound tasks | Good for I/O-bound tasks | Excellent (millions of tasks simultaneously) |
| Typical Scenario | Educational examples, simple services | Recursive calculations, e.g., sorting | Integrations, API requests, reactive chains | Servers, microservices, massive connections |
| Compatibility with Old Code | Full | Requires adaptation | Compatible with Executors | Full, with no API changes |
| When to Use | When simplicity and control are needed | When CPU performance is important | When an asynchronous API is needed without blocking | When scaling millions of tasks is required |
| When Not to Use | Under high load | For I/O-bound operations | For large computations | If precise OS thread control is needed |
This table helps understand the evolution: from manual threads, through smart task pools, to a new era of lightweight virtual threads. Today, the best approach is to combine tools: use Fork/Join for computations, CompletableFuture for asynchronous chains, and Project Loom — when scalability is needed without architectural complexity.
1️⃣ Fork/Join Framework
Fork/Join Framework, introduced in Java 7, is a specialized thread pool for tasks of the "divide and conquer" (divide and conquer) type. A large task is divided into smaller subtasks that are executed in parallel, and then merged into a common result.
Key elements:
- ForkJoinPool — a thread pool optimized for recursive and small tasks.
- RecursiveTask<V> and RecursiveAction — classes for tasks with a return result and without it.
- Work-Stealing — a balancing mechanism where threads steal tasks from other threads, to avoid idleness and ensure maximum CPU utilization.
Example:
class FibonacciTask extends RecursiveTask<Integer> {
final int n;
FibonacciTask(int n) { this.n = n; }
@Override
protected Integer compute() {
if (n <= 1) return n;
FibonacciTask f1 = new FibonacciTask(n - 1);
f1.fork();
FibonacciTask f2 = new FibonacciTask(n - 2);
return f2.compute() + f1.join();
}
}
Fork/Join is ideally suited for CPU-bound tasks — computations that can be broken down into independent blocks. However, excessive recursion can lead to overhead, and blocking operations reduce the pool's efficiency.
Operation diagram:
A large task → divided into subtasks → each subtask is executed in its own thread → results are combined.
Work-Stealing: threads with empty queues steal tasks from busy threads — maximum CPU efficiency.
2️⃣ CompletableFuture — asynchronous programming with functional style
With the arrival of Java 8, the language introduced CompletableFuture — a powerful tool for asynchronous programming. It allows you to create chains of dependent operations, handle errors, and perform tasks non-blockingly, which is especially useful for I/O-bound tasks.
Main features:
- thenApply, thenCompose, thenCombine — composition and combination of asynchronous tasks.
- exceptionally, handle — flexible error handling.
- supplyAsync, runAsync — executing tasks in a background thread pool (by default —
ForkJoinPool.commonPool()).
Example of a CompletableFuture chain:
CompletableFuture.supplyAsync(() -> fetchUserFromDb(userId))
.thenCompose(user -> fetchUserOrders(user))
.thenAccept(orders -> sendEmail(userId, orders))
.exceptionally(ex -> {
log.error("Error fetching orders", ex);
return null;
});
The main advantage of CompletableFuture is the ability to not block threads. It eliminates the "one thread per request" model, allowing for easy scaling of systems that work with networks or files.
However, it is important to remember: the common pool ForkJoinPool.commonPool() can be blocked during intensive I/O tasks, so it's better to use your own Executor or move to a more modern model — virtual threads.
Diagram of the asynchronous chain:
Data flow: supplyAsync → thenApply → thenCompose → thenAccept → error handling via exceptionally.
3️⃣ Virtual Threads (Project Loom)
Project Loom — a revolution in the Java multithreading model. Virtual threads are lightweight threads managed by the JVM, rather than the operating system. They allow creating thousands and even millions of parallel tasks with minimal resource overhead.
Advantages:
- Scalability — thousands of tasks without fine-tuning thread pools.
- simplicity — familiar synchronous code works in a non-blocking way.
- Compatibility — support for older APIs:
synchronized,ReentrantLock,Blocking I/O.
Example of Virtual Threads:
import java.util.List;
import java.util.concurrent.*;
import java.util.stream.IntStream;
public class VirtualThreadsExample {
// Symbolic blocking operation — simulating I/O (e.g., HTTP request)
static String doBlockingCall(int i) throws InterruptedException {
Thread.sleep(100); // block the thread for 100 ms
return "Result " + i + " from " + Thread.currentThread();
}
public static void main(String[] args) throws Exception {
// ✅ Create a virtual pool — each task will get its own virtual thread.
// This is the key feature of Project Loom.
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
try {
// Create 1000 tasks — regular threads would "choke" the system,
// but virtual threads work smoothly, as they are managed by the JVM.
List<Future<String>> futures = IntStream.range(0, 1000)
.mapToObj(i -> executor.submit(() -> doBlockingCall(i)))
.toList();
// Get results — Future.get() blocks the thread,
// but Loom parks the virtual thread without occupying the system one.
for (Future<String> f : futures) {
System.out.println(f.get());
}
} finally {
// Shutdown the Executor to properly terminate execution.
executor.shutdown();
}
}
}
Virtual threads are especially useful for mass I/O operations — for example, when processing HTTP requests or accessing external APIs. The code remains linear and readable, without callback hell.
Comparison of Regular and Virtual Threads:
Regular threads: thousands → high memory load.
Virtual threads: millions → minimal overhead, the JVM manages locks itself.
4️⃣ How to Choose the Right Tool
| Task Type | Recommended Tool | Reason |
|---|---|---|
| CPU-bound computations | Fork/Join Framework | Divide-and-conquer and effective CPU loading |
| I/O-bound asynchronous chains | CompletableFuture | Asynchronicity and functional composition without blocking threads |
| Mass I/O and high parallelism | Virtual Threads (Project Loom) | Scalability and simplicity of synchronous code |
In practice, a combination of approaches is often used: Fork/Join — for computations, CompletableFuture — for integration with external services, Loom — for scalable I/O operations.
Transition from Threads to Modern Tools
In the previous example, we created three threads manually. This helped to speed up order processing, but as the load increases, this approach quickly becomes a problem. If a thousand orders come in — we cannot start a thousand threads: the system will simply hit a limit.
"A thread is like if each order were handled by a separate employee. But if there are too many customers, you'll have to hire an army of people. Instead, we need a smart manager who distributes the tasks himself."
In Java, this role is performed by higher-level tools: ExecutorService, ForkJoinPool, and CompletableFuture. They manage a thread pool — that is, they keep a limited number of workers and assign them tasks as they become available.
Example using CompletableFuture:
import java.util.concurrent.CompletableFuture;
public class OrderProcessingSmart {
public static void main(String[] args) {
CompletableFuture
order1 = CompletableFuture.runAsync(() -> processOrder("Order #1"));
CompletableFuture
order2 = CompletableFuture.runAsync(() -> processOrder("Order #2"));
CompletableFuture
order3 = CompletableFuture.runAsync(() -> processOrder("Order #3"));
CompletableFuture.allOf(order1, order2, order3).join();
System.out.println("All orders processed!");
}
private static void processOrder(String name) {
System.out.println(name + " started " + Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(name + " completed " + Thread.currentThread().getName());
}
}
Here, there is no need to manually create threads — the system itself decides how many workers to start, when to start them, and how to wait for all results. The code has become shorter, safer, and ready for real load.
On what basis CompletableFuture makes decisions
When you call CompletableFuture.runAsync() without specifying your own Executor, Java takes the tasks and sends them to the common pool — ForkJoinPool.commonPool(). This is a special mechanism that appeared in Java 7, which optimizes CPU usage.
Simply put, the JVM keeps several worker threads — usually as many as the processor has cores. And as soon as one thread is free, it "steals" a task from the queue of another thread (the Work-Stealing algorithm). This achieves almost full CPU utilization without idle time.
This is the fundamental difference from new Thread(). When you create threads manually, each thread lives on its own, takes up memory, and the JVM doesn’t know how to balance them. A pool is like a dispatcher: it monitors the state of the threads and decides for itself who to assign which task.
CompletableFuture.runAsync(() -> processOrder("Order #1")); // executes inside ForkJoinPool.commonPool()
But you can be even smarter: if you need to control the behavior of the pool — for example, allocate more threads for I/O or limit CPU-bound tasks, you pass your own ExecutorService:
ExecutorService executor = Executors.newFixedThreadPool(4); CompletableFuture.runAsync(() -> processOrder("Order #1"), executor); CompletableFuture.runAsync(() -> processOrder("Order #2"), executor);
Now the decisions about distribution are made not by the common JVM pool, but by your dedicated pool of four threads — this is already a business setting for a specific load or microservice.
So Java decides not at the level of "guessing what is better", but at the level of "maximally efficiently using available cores and tasks in the queue".
When Project Loom comes with virtual threads, the logic will remain the same, but the scheduler will be able to hold millions of tasks because virtual threads themselves do not take up system resources while waiting for I/O.
Virtual Threads (Project Loom) — The Future of Multithreading in Java
Until recently, each system thread in Java was heavy — around a megabyte for the stack and significant overhead for context switching. This limited the practical number of concurrently live threads and made scaling through new Thread() costly.
A virtual thread is a lightweight thread managed by the JVM, not the OS. It allows you to create thousands and millions of tasks without critical memory overhead.
In Brief
Virtual threads are a modern version of "green threads": their creation and scheduling are handled by the JVM, so switching and waiting for I/O is cheaper. The program logic remains synchronous but scales like asynchronous code.
Example: Regular Threads vs Virtual Threads
Regular Threads:
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName());
}).start();
}
Virtual Threads:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10000; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName());
});
}
}
Key Advantages
- Ease of Creation: individual virtual threads require significantly less memory.
- Scalability: can handle hundreds of thousands — millions of parallel tasks.
- Compatibility: the same API
Thread/Executor, minimal changes to the code. - Simple: write regular synchronous code, achieving asynchronous behavior.
When to Use
Ideal for mass I/O: web servers, REST APIs, microservices with a high number of network calls and file operations.
Virtual Threads work great when the program is waiting — for example, while waiting for a response from a database, network, or file. At this moment, the thread can be "frozen" and frees the CPU for other tasks. But if the code is not waiting and is constantly computing (mathematics, hashing, video processing), then virtual threads won’t help — because each task occupies the CPU continuously, not yielding to others.Limitations and Pitfalls
- Virtual threads do not speed up CPU-bound tasks — for heavy computations, Fork/Join or specialized pools are needed.
- Old libraries with native blocking code (sometimes) can interfere — it’s worth testing drivers and dependencies.
- Monitoring and profiling tools older than JDK21 may show different behavior — check for support in your stack.
Practical Migration
In many cases, it is enough to replace ExecutorService with virtual-based, and existing synchronous code will start to scale better:
var executor = Executors.newVirtualThreadPerTaskExecutor();
try {
for (int i = 0; i < 10000; i++) {
executor.submit(() -> httpCall());
}
} finally {
executor.shutdown();
}
Often transitioning to Loom requires minimal changes to logic and provides a significant gain in throughput where the application waits heavily for I/O.
Summary
Virtual threads are a powerful tool for modern I/O-intensive systems: easy to use, compatible with the current API, and offering high scalability. However, don’t forget about combining: Fork/Join and specialized pools remain relevant for heavy computations.
Virtual Streams Loom as a Metaverse
Regular streams are real buildings. Virtual streams are like apartments in the metaverse: millions of objects, almost no costs, and real resources are only needed when actual work is done.
With Loom, thousands and millions of streams are like a skyscraper of virtual apartments: they weigh almost nothing and only actually work when needed.
Comparison of Approaches to Parallelism in Java and Prerequisites for Choosing
| Approach | Type of Tasks | Scalability | Code Complexity | Resources (CPU/Memory) | Prerequisites for Choosing |
|---|---|---|---|---|---|
| Regular Threads (Thread) | CPU-bound or simple I/O | Up to hundreds of threads | Simple | High memory load with a large number of threads | Few tasks, strict control over threads, simple synchronization |
| Fork/Join Framework | CPU-bound, divide-and-conquer | Hundreds of threads efficiently | Medium, requires task splitting | Optimized for small tasks, work-stealing | Large computations with the possibility of recursive splitting into subtasks |
| CompletableFuture | I/O-bound, asynchronous chains | Thousands of tasks when using pools | Medium/High (callback, composition) | Depends on the thread pool, I/O blocking reduces efficiency | Many asynchronous operations, need to combine results, error handling |
| Virtual Threads (Project Loom) | I/O-bound, millions of parallel tasks | Tens of thousands — millions of threads | Low, code remains synchronous | Low memory load, effective JVM lock management | Mass I/O operations, HTTP services, external service calls, multi-million connections |
Conclusion
Java has evolved from classic multithreading to modern tools, capable of handling millions of parallel tasks. Mastery of all three approaches — Fork/Join Framework, CompletableFuture and Project Loom — distinguishes a mature developer who understands the nature of parallel computing, resource management, and the architecture of scalable systems.
An expert who masters these tools can:
— create efficient CPU algorithms;
— build asynchronous reactive chains;
— scale I/O-intensive applications to millions of connections.
Оставить комментарий
Useful Articles:
New Articles: