CodeGym /Cursos /JAVA 25 SELF /Mejores prácticas de programación paralela

Mejores prácticas de programación paralela

JAVA 25 SELF
Nivel 54, Lección 4
Disponible

1. No todo lo que se puede paralelizar debe paralelizarse

En Java hay muchas formas de paralelizar tareas. Pero «paralelismo = siempre más rápido» es como pensar que si añades más sal a la sopa, sabrá mejor: hasta cierto punto — sí, más allá — mejor no intentarlo.

ExecutorService es perfecto cuando tienes tareas explícitas que lanzar y controlar: procesamiento de solicitudes, carga asíncrona de datos, cálculos independientes. Tú decides cuántos hilos habrá en el pool y controlas el ciclo de vida de las tareas.

parallelStream es una forma rápida de paralelizar el procesamiento de colecciones cuando las operaciones son independientes y sin efectos secundarios. Compensa para colecciones «pesadas» (decenas de miles de elementos o más).

ForkJoinPool es la elección para tareas que se dividen bien en subtareas (divide y vencerás): ordenación, búsqueda, agregación de grandes arrays. Se usa por dentro de parallelStream, pero también puedes controlarlo directamente.

No uses paralelismo «por si acaso». Si la tarea es pequeña, la sobrecarga de planificación, cambio de contexto y sincronización puede comerse toda la ganancia.

Ejemplo: cuándo no hace falta paralelismo

List<Integer> smallList = List.of(1, 2, 3, 4, 5);
int sum = smallList.parallelStream()
    .mapToInt(x -> x)
    .sum(); // Paralelizar por 5 números es overkill!

2. Thread-safety: evitamos los datos mutables compartidos

En el mundo paralelo, la principal amenaza son las carreras de datos (race conditions). Si varios hilos cambian una misma variable, el resultado puede ser inesperado.

  • Evita variables compartidas mutables. Incluso una expresión como counter++ no es atómica.
  • Usa colecciones thread-safe y operaciones atómicas. Por ejemplo, ConcurrentHashMap, CopyOnWriteArrayList, AtomicInteger, AtomicLong.
  • Sin efectos secundarios en streams paralelos. No modifiques estructuras externas desde parallelStream.

Ejemplo de mal código

List<Integer> numbers = Arrays.asList(1,2,3,4,5);
List<Integer> result = new ArrayList<>();
numbers.parallelStream().forEach(n -> result.add(n * 2)); // ¡PELIGROSO!

Aquí result.add() no es thread-safe. Resultado: elementos perdidos o excepciones.

¿Cómo hacerlo bien?

List<Integer> result = numbers.parallelStream()
    .map(n -> n * 2)
    .collect(Collectors.toList());

3. Rendimiento: más hilos no siempre es mejor

No sale a cuenta paralelizar tareas pequeñas. Si el trabajo dura milisegundos, lanzar en paralelo a menudo solo ralentizará la ejecución por la sobrecarga.

Mide el rendimiento. Para mediciones rápidas vale System.nanoTime():

long start = System.nanoTime();
// ... tu código ...
long end = System.nanoTime();
System.out.println("Tiempo de ejecución: " + (end - start) + " ns");

Para microbenchmarks serios usa JMH (Java Microbenchmark Harness).

Ejemplo: comparación de stream secuencial y paralelo

List<Integer> bigList = IntStream.range(0, 1_000_000)
    .boxed().collect(Collectors.toList());

long t1 = System.nanoTime();
long sum1 = bigList.stream().mapToLong(x -> x).sum();
long t2 = System.nanoTime();
long sum2 = bigList.parallelStream().mapToLong(x -> x).sum();
long t3 = System.nanoTime();

System.out.println("Secuencial: " + (t2 - t1) / 1_000_000 + " ms");
System.out.println("Paralelo:     " + (t3 - t2) / 1_000_000 + " ms");

Pruébalo en tu ordenador — la ventaja se nota en colecciones realmente grandes y operaciones pesadas.

4. Gestión de errores: no ignores las excepciones en hilos

Future y gestión de excepciones

Si lanzaste una tarea mediante ExecutorService.submit(), las excepciones no se propagan automáticamente — hay que gestionarlas a través de Future.get():

Future<Integer> future = executor.submit(() -> {
    if (Math.random() > 0.5) throw new RuntimeException("¡Ups!");
    return 42;
});
try {
    Integer result = future.get(); // puede lanzar ExecutionException
} catch (ExecutionException e) {
    System.err.println("Error en la tarea: " + e.getCause());
}

ForkJoin y gestión de errores

En ForkJoinPool las excepciones se «empaquetan» en la tarea. Al llamar a join()/get() saldrán a la superficie:

ForkJoinPool pool = new ForkJoinPool();
RecursiveTask<Integer> task = new MyTask();
try {
    int result = pool.invoke(task);
} catch (Exception e) {
    System.err.println("Error en ForkJoin: " + e);
}

No olvides gestionar InterruptedException

Muchos métodos (por ejemplo, Future.get(), Thread.sleep()) pueden lanzar InterruptedException. No lo «tragues» — reacciona correctamente: restablece el flag de interrupción o termina la tarea.

5. Depuración y pruebas del código paralelo

Registro y depuración

Los bugs paralelos son traicioneros y a menudo se manifiestan de forma inestable. Registra indicando el hilo: Thread.currentThread().getName(). Esto ayuda a entender quién y cuándo ejecuta el código.

En casos complejos usa un depurador con soporte de multihilo (por ejemplo, IntelliJ IDEA). Dormidos temporales con Thread.sleep() a veces ayudan a «capturar» una carrera de datos rara.

Pruebas de escenarios multihilo

Dedica pruebas específicas a las operaciones paralelas y usa utilidades de espera de condiciones, por ejemplo Awaitility. Ejecuta esas pruebas muchas veces: algunos problemas solo aparecen en la 100.ª o 1000.ª ejecución.

6. Matices y consejos útiles

Legibilidad y mantenibilidad: escribe código paralelo comprensible

  • Documenta. Comenta las partes complejas y la elección de herramientas.
  • Usa abstracciones de alto nivel. Prefiere ExecutorService, parallelStream, ForkJoinPool en lugar de la gestión manual de hilos.
  • Evita la «magia». No compliques la sincronización si se puede resolver de forma más simple.

Tabla: cuándo usar cada herramienta

Escenario Herramienta recomendada
Muchas tareas independientes
ExecutorService
Procesamiento de una colección grande parallelStream o ForkJoin
Tarea «divide y vencerás»
ForkJoinPool + RecursiveTask
Tarea asíncrona simple
CompletableFuture
Muchas tareas pequeñas Stream secuencial
Tareas con efectos secundarios ¡Solo colecciones thread-safe!

«Mandamientos» del programador paralelo

  • No tengas variables compartidas mutables — salvo que estés seguro de que son thread-safe.
  • No paralelices por paralelizar: valora la ganancia potencial.
  • No olvides cerrar los pools: shutdown()/shutdownNow().
  • No uses parallelStream para operaciones con efectos secundarios.
  • No olvides gestionar las excepciones de Future y ForkJoinTask.
  • No «tragues» InterruptedException — termina la tarea correctamente.

7. Errores típicos en la programación paralela

Error n.º 1: paralelizar tareas pequeñas. A menudo los principiantes paralelizan todo, incluso cuando el trabajo dura microsegundos. Resultado: más lento por la sobrecarga.

Error n.º 2: efectos secundarios en streams. En parallelStream no se pueden modificar variables o colecciones externas — aparecerán carreras de datos y bugs impredecibles.

Error n.º 3: ignorar excepciones. Si no gestionas los errores de Future.get() o ForkJoinTask, no sabrás por qué la tarea no se ejecutó.

Error n.º 4: olvidar shutdown() en ExecutorService. Sin cierre explícito, la aplicación puede «colgarse» al salir.

Error n.º 5: usar colecciones no thread-safe. Escribir desde varios hilos en un ArrayList normal es un camino directo a los errores.

Error n.º 6: «tragarse» InterruptedException. Si un hilo fue interrumpido — respétalo y termina el trabajo correctamente.

Error n.º 7: lógica de sincronización demasiado compleja. Los bloques synchronized-bloques llevan a deadlock/livelock. Prefiere abstracciones de alto nivel.

1
Tarea
JAVA 25 SELF, nivel 54, lección 4
Bloqueada
Experimento de magia caótica con duplicación de artefactos 🧪
Experimento de magia caótica con duplicación de artefactos 🧪
1
Tarea
JAVA 25 SELF, nivel 54, lección 4
Bloqueada
Análisis de antiguos pergaminos en la biblioteca digital 📜
Análisis de antiguos pergaminos en la biblioteca digital 📜
1
Cuestionario/control
Paralelismo y ForkJoin, nivel 54, lección 4
No disponible
Paralelismo y ForkJoin
Paralelismo y ForkJoin
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION