Introducción: de los synchronized básicos a las herramientas modernas
Java es uno de los pocos lenguajes que desde su creación tiene soporte integrado para multihilo. Sin embargo, a nivel inicial, los desarrolladores a menudo se limitan a la palabra clave synchronized y a los simples Thread. En sistemas reales de alta carga, esto no es suficiente: los bloqueos generan interbloqueos, y la creación ingenua de hilos provoca una sobrecarga de la JVM.
En este artículo, analizaremos los mecanismos avanzados de concurrencia en Java que permiten escribir código multihilo eficiente, escalable y seguro. El material está dirigido a desarrolladores que ya conocen los fundamentos del multihilo (monitores, wait/notify, volatile).
Cubriremos cuatro temas clave: Fork/Join Framework para tareas recursivas, CompletableFuture para programación asíncrona, StampedLock como alternativa a ReadWriteLock y el uso adecuado de ThreadLocal.
1. Fork/Join Framework: división recursiva de tareas
Fork/Join Framework apareció en Java 7 y está diseñado para aprovechar eficientemente los procesadores multinúcleo al resolver tareas que se pueden dividir en subtareas (divide y vencerás). La clase principal es ForkJoinPool, que utiliza el mecanismo de "robo de tareas" (work-stealing).
1.1. Principio de funcionamiento
En lugar de crear hilos manualmente, describes una tarea como heredera de RecursiveTask (si tiene un valor de retorno) o RecursiveAction (sin retorno). Luego, divides recursivamente la tarea en partes hasta que sean lo suficientemente pequeñas para ejecutarlas directamente.
1.2. Ejemplo: suma paralela de un array
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(); // ejecutamos la parte izquierda de forma asíncrona long rightResult = right.compute(); // calculamos la derecha en el hilo actual long leftResult = left.join(); // esperamos el resultado de la izquierda 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("Suma: " + result); }}Importante: usa ForkJoinPool.commonPool() — ya está optimizado para la cantidad de núcleos. Evita crear pools propios a menos que sea necesario.
2. CompletableFuture: asincronía sin el infierno de los callbacks
Antes de Java 8, la programación asíncrona era posible mediante Future, pero era síncrona (llamada bloqueante get()). CompletableFuture resuelve este problema proporcionando una potente API funcional para la composición de operaciones asíncronas.
2.1. Escenarios básicos
Puedes crear CompletableFuture, ejecutar tareas de forma asíncrona y combinarlas en cadenas:
import java.util.concurrent.CompletableFuture;
public cl