1 - Introducción a la Concurrencia en Java

Java nació con soporte nativo para hilos y un modelo de memoria definido y documentado.
Esta primera unidad sienta las bases: diferencias entre concurrencia y paralelismo en la JVM, por qué usarlos en aplicaciones modernas y qué rasgos distinguen al modelo de hilos de Java frente a otras plataformas.

Dominar estas reglas evita efectos de visibilidad extraños y te ayuda a elegir las primitivas correctas desde el inicio.
Al final de la sección tendrás criterios prácticos para dimensionar pools y separar trabajo CPU-bound (el cuello está en el procesador) de I/O-bound (el cuello está en entrada/salida).

1.1 Concurrencia vs paralelismo en el ecosistema Java

  • JVM multithreaded: la máquina virtual está diseñada para ejecutar varios hilos nativos; el programador crea Thread, usa ExecutorService o CompletableFuture y la JVM los mapea a hilos del sistema operativo.
  • Modelo de memoria definido: el Java Memory Model establece reglas de visibilidad y reordenamiento; garantiza efectos happens-before cuando usamos synchronized, volatile o utilidades de java.util.concurrent.
  • Concurrencia a nivel de aplicación: hilos que diseñas explícitamente (pools, tareas reactivas, flujos en paralelo).
  • Concurrencia dentro de la VM: la JVM también ejecuta hilos propios (GC, JIT, monitorización). Entenderlos ayuda a interpretar perfiles y priorizar la afinidad de CPU o el número de threads en tus pools.
  • Concurrencia vs paralelismo: concurrencia es intercalar tareas para mantener responsividad; paralelismo es ejecutar al mismo tiempo en varios núcleos. Java permite ambos, pero dependen del planificador del SO y del diseño de tus tareas.

Al diseñar APIs que expondrán hilos de usuario recuerda que los hilos del recolector y del JIT también consumen CPU.
Si usas contenedores o funciones serverless, valida cuántos hilos nativos están permitidos para evitar throttling o pausas inesperadas.

1.2 Razones para usar concurrencia

  • Servidores web: contenedores como Tomcat o Netty atienden muchas peticiones en paralelo; los pools de hilos evitan bloqueos en operaciones de negocio.
  • Aplicaciones reactivas: frameworks como Spring WebFlux o Vert.x combinan hilos y event loops para mantener latencias bajas.
  • Procesamiento masivo: pipelines de datos, ETL y streams paralelos (parallelStream()) distribuyen trabajo entre núcleos.
  • Tareas CPU-bound e I/O-bound: elegir el tamaño correcto del pool separa el cálculo intensivo del trabajo de entrada/salida.
  • Miles de conexiones: NIO (New I/O) y canales asíncronos permiten multiplexar sockets con pocos hilos, liberando recursos frente a modelos bloqueantes puros.

Divide tus pools: uno optimizado para I/O (muchos hilos) y otro reducido para CPU (hilos cercanos a la cantidad de núcleos).
Para cargas mixtas, usa colas diferenciadas y mide la latencia percibida antes de ajustar el tamaño de los ejecutores.

import java.time.Instant;
import java.util.concurrent.*;

public class EjecutorBasico {
  public static void main(String[] args) throws Exception {
    ExecutorService pool = Executors.newFixedThreadPool(4);

    Callable<String> ioBound = () -> {
      TimeUnit.MILLISECONDS.sleep(300); // Simula I/O
      return "Listo I/O @ " + Instant.now();
    };

    Callable<Long> cpuBound = () ->
        java.util.stream.LongStream.rangeClosed(1, 5_000_000L).sum();

    Future<String> respuestaIo = pool.submit(ioBound);
    Future<Long> respuestaCpu = pool.submit(cpuBound);

    System.out.println(respuestaIo.get());
    System.out.println("Suma CPU: " + respuestaCpu.get());
    pool.shutdown();
  }
}

El ejemplo usa un ExecutorService fijo para mezclar tareas bloqueantes y cálculo; la JVM reparte los hilos sobre los núcleos disponibles y el programador controla el tamaño del pool.

1.3 Características únicas del modelo de hilos en Java

  • Scheduling mixto: el sistema operativo planifica hilos nativos, mientras la JVM puede optimizar hotspots y aplicar Thread.yield() o prioridades para influir en el reparto.
  • Memoria compartida: todos los hilos dentro del mismo proceso Java acceden al mismo heap; comunicar datos es barato pero requiere sincronización explícita.
  • Visibilidad y reordenamiento: el JMM permite reordenar instrucciones; las barreras implícitas de synchronized, volatile y Lock aseguran que las escrituras sean observables en otros hilos.
  • Sincronización estructurada: APIs de java.util.concurrent (colas bloqueantes, CountDownLatch, CompletableFuture) reducen errores de bajo nivel y proporcionan garantías de orden.
  • Colaboración con el GC: colectores modernos son paralelos y/o concurrentes; ejecutan hilos propios que pueden competir por CPU, por lo que conviene dimensionar pools según la carga real.

Documenta qué variables requieren volatile o bloqueos y evita exponer referencias mutables sin protección.
Aplica perfiles con jconsole o jcmd para ver cuánto tiempo pasan los hilos en estados bloqueados y ajustar tu estrategia.