Introduction: From Basic synchronized to Modern Tools
Java is one of the few languages that has had built-in multithreading support since its inception. However, at the initial level, developers often limit themselves to the synchronized keyword and simple Thread objects. In real high-load systems, this is not enough: locks cause deadlocks, and naive thread creation leads to JVM overload.
In this article, we will explore advanced concurrency mechanisms in Java that allow you to write efficient, scalable, and safe multithreaded code. The material is intended for developers already familiar with the basics of multithreading (monitors, wait/notify, volatile).
We will cover four key topics: Fork/Join Framework for recursive tasks, CompletableFuture for asynchronous programming, StampedLock as an alternative to ReadWriteLock, and the proper use of ThreadLocal.
1. Fork/Join Framework: Recursive Task Decomposition
The Fork/Join Framework appeared in Java 7 and is designed for efficient use of multi-core processors when solving problems that can be divided into subtasks (divide and conquer). The main class is ForkJoinPool, which uses a work-stealing mechanism.
1.1. How It Works
Instead of creating threads manually, you describe a task as a subclass of RecursiveTask (if there is a return value) or RecursiveAction (no return value). You then recursively split the task into parts until they are small enough to be executed directly.
1.2. Example: Parallel Array Summation
import java.util.concurrent.RecursiveTask;import java.util.concurrent.ForkJoinPool;
public class SumTask extends RecursiveTask<Long> { private static final int THRESHOLD = 10_000; private final int[] array; private final int start, end;
public SumTask(int[] array, int start, int end) { this.array = array; this.start = start; this.end = end; }
@Override protected Long compute() { if (end - start <= THRESHOLD) { long sum = 0; for (int i = start; i < end; i++) { sum += array[i]; } return sum; } int mid = start + (end - start) / 2; SumTask left = new SumTask(array, start, mid); SumTask right = new SumTask(array, mid, end); left.fork(); // asynchronously run the left part long rightResult = right.compute(); // compute the right part in the current thread long leftResult = left.join(); // wait for the left part result return leftResult + rightResult; }
public static void main(String[] args) { int[] data = new int[1_000_000]; for (int i = 0; i < data.length; i++) data[i] = i;
ForkJoinPool pool = ForkJoinPool.commonPool(); long result = pool.invoke(new SumTask(data, 0, data.length)); System.out.println("Sum: " + result); }}Important: use ForkJoinPool.commonPool() — it is already optimized for the number of cores. Avoid creating custom pools unnecessarily.
2. CompletableFuture: Asynchrony Without Callback Hell
Before Java 8, asynchronous programming was possible via Future, but it was synchronous (blocking get() call). CompletableFuture solves this problem by providing a powerful functional API for composing asynchronous operations.
2.1. Basic Scenarios
You can create CompletableFuture, run tasks asynchronously, and chain them together:
import java.util.concurrent.CompletableFuture;
public cl