Java
Tutorial Java: Streams paralelos
Aprende a usar parallelStream en Java para procesar colecciones concurrentemente y mejorar el rendimiento en operaciones con grandes datos.
Aprende Java y certifícateparallelStream()
El método parallelStream() es una característica introducida en Java 8 que permite procesar colecciones de datos de forma concurrente, aprovechando los múltiples núcleos de procesamiento disponibles en los equipos modernos. Esta funcionalidad forma parte del paradigma de la programación funcional en Java y está diseñada para mejorar el rendimiento en operaciones sobre grandes conjuntos de datos.
La API de Stream en Java ofrece dos tipos de procesamiento: secuencial (donde los elementos se procesan uno tras otro) y paralelo (donde los elementos se procesan concurrentemente). El método parallelStream()
es una forma directa de crear un stream paralelo a partir de una colección.
Creación de Streams paralelos con parallelStream()
Para crear un stream paralelo a partir de una colección, simplemente invocamos el método parallelStream()
en lugar del tradicional stream()
:
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Creación de un stream paralelo
Stream<Integer> streamParalelo = numeros.parallelStream();
Este método está disponible en todas las clases que implementan la interfaz Collection
, lo que lo hace muy accesible para la mayoría de las estructuras de datos en Java.
Uso básico de parallelStream()
Veamos un ejemplo sencillo donde calculamos la suma de los cuadrados de una lista de números:
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Usando parallelStream para calcular la suma de cuadrados
int suma = numeros.parallelStream()
.map(n -> n * n)
.reduce(0, Integer::sum);
System.out.println("La suma de los cuadrados es: " + suma);
En este ejemplo, la operación map
que calcula el cuadrado de cada número se ejecuta en paralelo, potencialmente en diferentes hilos, y luego los resultados se combinan mediante la operación reduce
.
Operaciones terminales comunes con parallelStream()
Las operaciones terminales funcionan de la misma manera que con streams secuenciales, pero se ejecutan en paralelo:
List<String> nombres = Arrays.asList("Ana", "Juan", "Carlos", "María", "Pedro");
// forEach en paralelo (el orden no está garantizado)
nombres.parallelStream()
.forEach(nombre -> System.out.println("Procesando: " + nombre));
// Filtrado y recolección en paralelo
List<String> nombresFiltrados = nombres.parallelStream()
.filter(nombre -> nombre.length() > 4)
.collect(Collectors.toList());
// Búsqueda en paralelo
Optional<String> primerNombreLargo = nombres.parallelStream()
.filter(nombre -> nombre.length() > 4)
.findAny();
Consideraciones de rendimiento
El uso de parallelStream()
no siempre garantiza un mejor rendimiento. Hay varios factores a considerar:
- Tamaño de la colección: Para colecciones pequeñas, el costo de dividir el trabajo y combinar resultados puede superar los beneficios.
// Para colecciones pequeñas, el stream secuencial puede ser más eficiente
List<Integer> pequeña = Arrays.asList(1, 2, 3, 4, 5);
int resultado = pequeña.stream() // Mejor usar stream() en lugar de parallelStream()
.map(n -> n * 2)
.reduce(0, Integer::sum);
- Tipo de operaciones: Las operaciones que requieren mucho cálculo por elemento se benefician más del paralelismo.
// Operación computacionalmente intensiva que se beneficia del paralelismo
List<BigInteger> numeros = obtenerListaGrande();
List<BigInteger> resultados = numeros.parallelStream()
.map(n -> calcularFactorial(n)) // Operación costosa
.collect(Collectors.toList());
- Estructura de datos subyacente: Algunas colecciones como
ArrayList
se dividen más fácilmente que otras comoLinkedList
.
// ArrayList es más eficiente para streams paralelos que LinkedList
ArrayList<Integer> arrayList = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
int suma = arrayList.parallelStream()
.reduce(0, Integer::sum);
Ejemplo práctico: Procesamiento de una gran colección
Veamos un ejemplo más completo donde procesamos una colección grande de datos:
// Creamos una lista grande de números
List<Integer> numerosGrandes = new ArrayList<>();
for (int i = 0; i < 10_000_000; i++) {
numerosGrandes.add(i);
}
// Medimos el tiempo con stream secuencial
long inicio = System.currentTimeMillis();
long sumaSecuencial = numerosGrandes.stream()
.filter(n -> n % 2 == 0)
.mapToLong(n -> (long) n * n)
.sum();
long finSecuencial = System.currentTimeMillis();
// Medimos el tiempo con parallelStream
inicio = System.currentTimeMillis();
long sumaParalela = numerosGrandes.parallelStream()
.filter(n -> n % 2 == 0)
.mapToLong(n -> (long) n * n)
.sum();
long finParalelo = System.currentTimeMillis();
System.out.println("Tiempo secuencial: " + (finSecuencial - inicio) + " ms");
System.out.println("Tiempo paralelo: " + (finParalelo - inicio) + " ms");
Este ejemplo muestra cómo el procesamiento paralelo puede mejorar significativamente el rendimiento en operaciones sobre grandes conjuntos de datos.
Operaciones con estado y paralelismo
Es importante tener cuidado con las operaciones que mantienen estado cuando se utilizan streams paralelos, ya que pueden producir resultados inesperados:
// Las operaciones con estado como sorted pueden comportarse diferente en paralelo
List<Integer> desordenados = Arrays.asList(5, 2, 8, 1, 9, 3, 7, 4, 6);
// En un stream paralelo, el orden de ejecución de forEach no está garantizado
desordenados.parallelStream()
.sorted()
.forEach(n -> System.out.print(n + " ")); // Puede no mostrar en orden ascendente
System.out.println();
// Para garantizar el orden, usar forEachOrdered
desordenados.parallelStream()
.sorted()
.forEachOrdered(n -> System.out.print(n + " ")); // Garantiza el orden
El método parallelStream()
proporciona una forma sencilla pero potente de aprovechar el procesamiento paralelo en Java, permitiendo mejorar el rendimiento de operaciones sobre colecciones grandes cuando se utiliza correctamente.
Diferencia entre Stream paralelo y Stream secuencial
Los Streams en Java representan una secuencia de elementos sobre los que se pueden realizar diversas operaciones. La principal diferencia entre un Stream paralelo y uno secuencial radica en cómo se procesan estos elementos y el impacto que esto tiene en el rendimiento, comportamiento y resultados de las operaciones.
Modelo de ejecución
Un Stream secuencial procesa los elementos uno tras otro en un único hilo de ejecución. Las operaciones se aplican a cada elemento en orden, siguiendo una secuencia predecible:
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5);
numeros.stream() // Stream secuencial
.map(n -> n * 2)
.forEach(n -> System.out.println(Thread.currentThread().getName() + ": " + n));
Por otro lado, un Stream paralelo divide automáticamente los datos en múltiples partes que se procesan simultáneamente en diferentes hilos:
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5);
numeros.parallelStream() // Stream paralelo
.map(n -> n * 2)
.forEach(n -> System.out.println(Thread.currentThread().getName() + ": " + n));
Al ejecutar este código, notarás que el Stream paralelo utiliza diferentes hilos del pool común ForkJoinPool, mientras que el secuencial usa solo el hilo principal.
Determinismo y orden de ejecución
Una diferencia fundamental es el determinismo en el orden de procesamiento:
- Los Streams secuenciales garantizan que los elementos se procesan en el orden definido por la fuente de datos.
- Los Streams paralelos no ofrecen garantías sobre el orden de procesamiento, lo que puede afectar a operaciones como
forEach
:
// Stream secuencial - orden predecible
List<String> nombres = Arrays.asList("Ana", "Carlos", "Elena", "David");
nombres.stream()
.forEach(System.out::println); // Imprime: Ana, Carlos, Elena, David
// Stream paralelo - orden impredecible
nombres.parallelStream()
.forEach(System.out::println); // El orden puede variar en cada ejecución
Para mantener el orden en Streams paralelos, podemos usar forEachOrdered
:
nombres.parallelStream()
.forEachOrdered(System.out::println); // Mantiene el orden original
Conversión entre tipos de Streams
Es posible convertir entre ambos tipos de Streams durante una cadena de operaciones:
// De secuencial a paralelo
Stream<Integer> streamParalelo = Stream.of(1, 2, 3, 4, 5)
.parallel();
// De paralelo a secuencial
Stream<Integer> streamSecuencial = streamParalelo.sequential();
// Verificar si un Stream es paralelo
boolean esParalelo = streamParalelo.isParallel(); // true
Impacto en operaciones de reducción
Las operaciones de reducción como reduce()
se comportan de manera diferente según el tipo de Stream:
// Reducción secuencial - predecible
int sumaSecuencial = IntStream.range(1, 1000)
.reduce(0, (a, b) -> {
System.out.println("Combinando " + a + " y " + b);
return a + b;
});
// Reducción paralela - orden de combinación impredecible
int sumaParalela = IntStream.range(1, 1000)
.parallel()
.reduce(0, (a, b) -> {
System.out.println("Combinando " + a + " y " + b);
return a + b;
});
En el Stream paralelo, las combinaciones ocurren en un orden diferente y posiblemente con valores intermedios distintos.
Diferencias de rendimiento según el escenario
El rendimiento relativo entre ambos tipos de Streams varía según diferentes factores:
- Tamaño de los datos: Los Streams paralelos suelen ser más eficientes con colecciones grandes:
// Para colecciones pequeñas, el secuencial puede ser más rápido
List<Integer> pequeña = IntStream.range(1, 100).boxed().collect(Collectors.toList());
// Para colecciones grandes, el paralelo puede ofrecer mejor rendimiento
List<Integer> grande = IntStream.range(1, 10_000_000).boxed().collect(Collectors.toList());
- Tipo de operación: Las operaciones con alta intensidad computacional se benefician más del paralelismo:
// Operación simple - el paralelismo puede no compensar
List<Integer> numeros = obtenerNumeros();
long conteoSecuencial = numeros.stream().count();
// Operación compleja - el paralelismo puede mejorar significativamente
long sumaComplejaParalela = numeros.parallelStream()
.map(n -> calcularOperacionCostosa(n))
.reduce(0L, Long::sum);
- Estructura de datos subyacente: Algunas colecciones son más adecuadas para la división paralela:
// ArrayList - eficiente para paralelismo (acceso aleatorio rápido)
ArrayList<Integer> arrayList = new ArrayList<>(numeros);
// LinkedList - menos eficiente para paralelismo (división más costosa)
LinkedList<Integer> linkedList = new LinkedList<>(numeros);
Efectos secundarios y operaciones con estado
Las operaciones con efectos secundarios pueden comportarse de manera impredecible en Streams paralelos:
// Problema con efectos secundarios en Stream paralelo
List<Integer> resultados = new ArrayList<>();
numeros.parallelStream()
.map(n -> n * 2)
.forEach(resultados::add); // Problema: ArrayList no es thread-safe
// Solución: usar un collector thread-safe
List<Integer> resultadosCorrectos = numeros.parallelStream()
.map(n -> n * 2)
.collect(Collectors.toList());
Las operaciones con estado como sorted()
, distinct()
o limit()
pueden tener diferentes características de rendimiento:
// En streams secuenciales, limit es una operación de corto circuito eficiente
long primerosSecuencial = IntStream.range(1, 1_000_000)
.filter(n -> n % 2 == 0)
.limit(10)
.count(); // Procesa solo los elementos necesarios
// En streams paralelos, limit puede procesar más elementos de los necesarios
long primerosParalelo = IntStream.range(1, 1_000_000)
.parallel()
.filter(n -> n % 2 == 0)
.limit(10)
.count(); // Puede procesar muchos más elementos
Escenarios de uso recomendados
La elección entre Streams secuenciales y paralelos debe basarse en el contexto específico:
- Usar Streams secuenciales cuando:
- La colección es pequeña (menos de miles de elementos)
- Las operaciones son simples y rápidas
- El orden de procesamiento es crítico
- Se trabaja con operaciones con estado como
limit()
ofindFirst()
- Usar Streams paralelos cuando:
- La colección es grande (miles o millones de elementos)
- Las operaciones son computacionalmente intensivas
- La fuente de datos se puede dividir eficientemente (como ArrayList o arrays)
- Las operaciones son independientes entre sí (sin efectos secundarios)
Ejemplo comparativo de rendimiento
Este ejemplo muestra la diferencia de rendimiento entre ambos enfoques en un caso realista:
// Generamos una lista grande de números
List<Integer> numeros = IntStream.range(0, 10_000_000)
.boxed()
.collect(Collectors.toList());
// Función que simula un cálculo intensivo
Function<Integer, Double> calculoIntensivo = num -> {
double resultado = 0;
for (int i = 0; i < 100; i++) {
resultado += Math.sin(Math.sqrt(num * Math.PI / i+1));
}
return resultado;
};
// Medición con stream secuencial
long inicioSec = System.currentTimeMillis();
double resultadoSec = numeros.stream()
.filter(n -> n % 2 == 0)
.mapToDouble(calculoIntensivo::apply)
.average()
.orElse(0);
long tiempoSec = System.currentTimeMillis() - inicioSec;
// Medición con stream paralelo
long inicioPar = System.currentTimeMillis();
double resultadoPar = numeros.parallelStream()
.filter(n -> n % 2 == 0)
.mapToDouble(calculoIntensivo::apply)
.average()
.orElse(0);
long tiempoPar = System.currentTimeMillis() - inicioPar;
System.out.println("Secuencial: " + tiempoSec + " ms");
System.out.println("Paralelo: " + tiempoPar + " ms");
System.out.println("Mejora: " + (tiempoSec / (double)tiempoPar) + "x");
En este tipo de escenario, con una colección grande y operaciones intensivas, el Stream paralelo puede ofrecer una mejora de rendimiento significativa, potencialmente proporcional al número de núcleos disponibles en el sistema.
Ejercicios de esta lección Streams paralelos
Evalúa tus conocimientos de esta lección Streams paralelos con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Streams: match
Gestión de errores y excepciones
CRUD en Java de modelo Customer sobre un ArrayList
Clases abstractas
Listas
Métodos de la clase String
Streams: reduce()
API java.nio 2
Polimorfismo
Pattern Matching
Streams: flatMap()
Llamada y sobrecarga de funciones
Métodos referenciados
Métodos de la clase String
Representación de Fecha
Operadores lógicos
Inferencia de tipos con var
Tipos de datos
Estructuras de iteración
Streams: forEach()
Objetos
Funciones lambda
Uso de Scanner
Tipos de variables
Streams: collect()
Operadores aritméticos
Arrays y matrices
Clases y objetos
Interfaz funcional Consumer
CRUD en Java de modelo Customer sobre un HashMap
Interfaces
Enumeraciones Enums
API Optional
Interfaz funcional Function
Encapsulación
Interfaces
Uso de API Optional
Representación de Hora
Herencia básica
Clases y objetos
Interfaz funcional Supplier
HashMap
Sobrecarga de métodos
Polimorfismo de tiempo de ejecución
OOP en Java
Sobrecarga de métodos
CRUD de productos en Java
Clases sealed
Creación de Streams
Records
Encapsulación
Streams: min max
Herencia
Métodos avanzados de la clase String
Funciones
Polimorfismo de tiempo de compilación
Reto sintaxis Java
Conjuntos
Estructuras de control
Recursión
Excepciones
Herencia avanzada
Estructuras de selección
Uso de interfaces
Operadores
Variables
HashSet
Objeto Scanner
Streams: filter()
Operaciones de Streams
Interfaz funcional Predicate
Streams: sorted()
Configuración de entorno
Uso de variables
Clases
Streams: distinct()
Streams: count()
ArrayList
Mapas
Datos de referencia
Interfaces funcionales
Métodos básicos de la clase String
Tipos de datos
Clases abstractas
Instalación
Funciones
Excepciones
Estructuras de control
Herencia de clases
La clase Scanner
Generics
Streams: map()
Funciones y encapsulamiento
Todas las lecciones de Java
Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Instalación De Java
Introducción Y Entorno
Configuración De Entorno Java
Introducción Y Entorno
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Recursión
Sintaxis
Arrays Y Matrices
Sintaxis
Excepciones
Programación Orientada A Objetos
Clases Y Objetos
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Clases Abstractas
Programación Orientada A Objetos
Interfaces
Programación Orientada A Objetos
Sobrecarga De Métodos
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
La Clase Scanner
Programación Orientada A Objetos
Métodos De La Clase String
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Records
Programación Orientada A Objetos
Pattern Matching
Programación Orientada A Objetos
Inferencia De Tipos Con Var
Programación Orientada A Objetos
Enumeraciones Enums
Programación Orientada A Objetos
Generics
Programación Orientada A Objetos
Clases Sealed
Programación Orientada A Objetos
Listas
Framework Collections
Conjuntos
Framework Collections
Mapas
Framework Collections
Funciones Lambda
Programación Funcional
Interfaz Funcional Consumer
Programación Funcional
Interfaz Funcional Predicate
Programación Funcional
Interfaz Funcional Supplier
Programación Funcional
Interfaz Funcional Function
Programación Funcional
Métodos Referenciados
Programación Funcional
Creación De Streams
Programación Funcional
Operaciones Intermedias Con Streams: Map()
Programación Funcional
Operaciones Intermedias Con Streams: Filter()
Programación Funcional
Operaciones Intermedias Con Streams: Distinct()
Programación Funcional
Operaciones Finales Con Streams: Collect()
Programación Funcional
Operaciones Finales Con Streams: Min Max
Programación Funcional
Operaciones Intermedias Con Streams: Flatmap()
Programación Funcional
Operaciones Intermedias Con Streams: Sorted()
Programación Funcional
Operaciones Finales Con Streams: Reduce()
Programación Funcional
Operaciones Finales Con Streams: Foreach()
Programación Funcional
Operaciones Finales Con Streams: Count()
Programación Funcional
Operaciones Finales Con Streams: Match
Programación Funcional
Api Optional
Programación Funcional
Transformación
Programación Funcional
Reducción Y Acumulación
Programación Funcional
Mapeo
Programación Funcional
Streams Paralelos
Programación Funcional
Agrupación Y Partición
Programación Funcional
Filtrado Y Búsqueda
Programación Funcional
Api Java.nio 2
Entrada Y Salida Io
Fundamentos De Io
Entrada Y Salida Io
Leer Y Escribir Archivos
Entrada Y Salida Io
Httpclient Moderno
Entrada Y Salida Io
Clases De Nio2
Entrada Y Salida Io
Api Java.time
Api Java.time
Localtime
Api Java.time
Localdatetime
Api Java.time
Localdate
Api Java.time
Executorservice
Concurrencia
Virtual Threads (Project Loom)
Concurrencia
Future Y Completablefuture
Concurrencia
Spring Framework
Frameworks Para Java
Micronaut
Frameworks Para Java
Maven
Frameworks Para Java
Gradle
Frameworks Para Java
Lombok Para Java
Frameworks Para Java
Quarkus
Frameworks Para Java
Ecosistema Jakarta Ee De Java
Frameworks Para Java
Introducción A Junit 5
Testing
Certificados de superación de Java
Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender qué es y cómo se crea un stream paralelo con parallelStream() en Java.
- Diferenciar entre streams secuenciales y paralelos en cuanto a ejecución, orden y rendimiento.
- Identificar cuándo es recomendable usar streams paralelos según el tamaño de la colección y la complejidad de las operaciones.
- Reconocer las consideraciones y limitaciones del paralelismo, como el manejo de operaciones con estado y efectos secundarios.
- Aplicar ejemplos prácticos para medir y comparar el rendimiento entre streams secuenciales y paralelos.