Concurrencia en Java: desde los flujos clásicos hasta gorutinas y async/await
\n\nJava es un lenguaje que desde su nacimiento ha sabido trabajar con múltiples tareas simultáneamente. Pero el mundo no se detiene. Hoy hablaremos sobre la concurrencia en Java — desde los clásicos flujos (Thread) hasta los flujos virtuales (gorutinas de Project Loom) y el enfoque asíncrono con async/await (usando CompletableFuture como ejemplo).
\n\nSi programas en Java y quieres que tu servidor no se "ralentice" bajo carga, mientras el código se mantiene legible, este artículo es para ti. ¡Allá vamos!
\n\n¿Por qué es importante la concurrencia para el desarrollador Java moderno?
\n\nImagina que tu sitio web procesa 10 000 solicitudes por segundo. Si cada solicitud se ejecuta secuencialmente, los usuarios esperarán una eternidad. La concurrencia en Java permite ejecutar múltiples tareas "simultáneamente", utilizando eficientemente los recursos del procesador.
\n\nLas aplicaciones modernas no son solo CRUD. Son microservicios, procesamiento de flujos de datos, WebSocket, análisis en tiempo real. Sin comprender el multihilo en Java, corres el riesgo de escribir código que fallará con OutOfMemoryError o funcionará como un caracol.
\n\nNivel 1: Flujos clásicos (Thread) — el fundamento
\n\nUn flujo en Java es la abstracción de más bajo nivel para ejecutar código en paralelo. Cada flujo es un "proceso ligero" independiente dentro del sistema operativo.
\n\nAsí se ve el ejemplo más simple de creación de un flujo:
\n\npublic class ThreadExample {\n public static void main(String[] args) {\n Thread thread = new Thread(() -> {\n for (int i = 0; i < 5; i++) {\n System.out.println("Hola desde el flujo: " + i);\n try {\n Thread.sleep(1000);\n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n }\n }\n });\n thread.start();\n System.out.println("El flujo principal continúa trabajando");\n }\n}\n\nVentajas:
\n- \n
- Simplicidad de creación — new Thread(() -> { ... }).start(); \n
- Control directo del ciclo de vida. \n
Desventajas:
\n- \n
- Crear un flujo es una operación costosa (gastos generales de pila, cambio de contexto). \n
- Limitación en cantidad: 10 000 flujos pueden matar tu JVM. \n
- Complejidad de sincronización: deadlock, race condition. \n
El enfoque clásico es bueno para tareas pequeñas, pero para aplicaciones de alta carga no es adecuado. Aquí es donde los flujos virtuales en Java vienen al rescate.
\n\nNivel 2: Flujos virtuales (Project Loom) — gorutinas en Java
\n\nSi has oído hablar de las gorutinas en Go, los flujos virtuales de Java son casi lo mismo. Aparecieron en Java 21 (Project Loom) y cambian completamente las reglas del juego.
\n\nLos flujos virtuales son flujos "ligeros" gestionados por la JVM, no por el sistema operativo. Se pueden crear por millones y consumen un mínimo de memoria.
\n\nEjemplo de creación de un flujo virtual:
\n\npublic class VirtualThreadExample {\n public static void main(String[] args) throws InterruptedException {\n // Creamos 100 000 flujos virtuales\n var threads = new Thread[100_000];\n for (int i = 0; i < threads.length; i++) {\n threads[i] = Thread.startVirtualThread(() -> {\n // Simulación de trabajo con I/O bloqueante\n try {\n Thread.sleep(1000);\n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n }\n });\n }\n for (Thread t : threads) {\n t.join();\n }\n System.out.println("¡Todos los flujos virtuales han terminado!");\n }\n}\n\nIncreíble, ¿verdad?