CodeGym /Cursos /JAVA 25 SELF /Recorridos paralelos del sistema de archivos: Files.walk ...

Recorridos paralelos del sistema de archivos: Files.walk + parallel() y ForkJoin

JAVA 25 SELF
Nivel 59 , Lección 2
Disponible

1. Problema: cómo procesar de forma eficiente muchos archivos en un directorio

En las aplicaciones modernas, a menudo surge la tarea: procesar una gran cantidad de archivos en una carpeta y sus subdirectorios. Por ejemplo:

  • Calcular el número total de líneas en todos los archivos ".java" del proyecto.
  • Encontrar todos los archivos modificados durante el último mes.
  • Copiar o eliminar archivos según un criterio determinado.

Si hay pocos archivos, basta con un bucle normal. Pero con miles y decenas de miles, especialmente cuando sobre cada archivo se realiza una operación «pesada» (lectura, parseo, análisis), el tiempo aumenta considerablemente.

Pregunta: ¿cómo acelerar el procesamiento de una gran cantidad de archivos?
Respuesta: usar paralelismo — procesar los archivos simultáneamente en varios hilos.

2. Herramientas para recorrer el sistema de archivos

Files.walk()

En Java 8+ apareció una forma cómoda de recorrer un árbol de directorios: el método Files.walk() del paquete java.nio.file. Devuelve un stream Stream<Path>: todos los archivos y carpetas, empezando por el directorio indicado.

Ejemplo:

import java.nio.file.*;
import java.util.stream.Stream;

Path start = Paths.get("src");
try (Stream<Path> stream = Files.walk(start)) {
    stream.forEach(System.out::println);
}
  • Files.walk(start) — devuelve un stream con todos los archivos y carpetas, incluidos los subdirectorios.
  • Se puede indicar la profundidad máxima del recorrido: Files.walk(start, 3).

Files.find()

Si necesitas filtrar inmediatamente por un criterio (por ejemplo, solo archivos ".java"), usa Files.find():

import java.nio.file.*;
import java.util.stream.Stream;

Path start = Paths.get("src");

try (Stream<Path> stream = Files.find(
        start,
        Integer.MAX_VALUE,
        (path, attr) -> path.toString().endsWith(".java"))) {
    stream.forEach(System.out::println);
}
  • Files.find() acepta un filtro (BiPredicate<Path, BasicFileAttributes>) que recibe la ruta y los atributos del archivo.

3. Procesamiento paralelo: parallel() y ForkJoinPool

Streams paralelos: .parallel()

Todo Stream tiene el método parallel(). Si lo llamas, el procesamiento de los elementos se realizará en varios hilos.

Files.walk(start)
    .parallel()
    .forEach(path -> processFile(path));

Cada archivo se procesará en paralelo (cuando sea posible), lo que es especialmente eficaz en operaciones «pesadas»: lectura, parseo y cálculos.

¿Cómo funciona por dentro? ForkJoinPool

Los streams paralelos usan un pool de hilos común — ForkJoinPool.commonPool(). Es un pool «inteligente» que distribuye las tareas entre los hilos.

  • Por defecto, el número de hilos es igual al de procesadores disponibles: Runtime.getRuntime().availableProcessors().
  • El modelo paralelo «fork/join» es adecuado para tareas independientes — como el procesamiento de archivos individuales.

¿Cuándo usar .parallel()?

  • Cuando el procesamiento de cada archivo es independiente del de los demás.
  • Cuando la operación es «pesada» (carga la CPU o espera mucho al IO).
  • Cuando hay muchos archivos (cientos, miles).

No conviene usar streams paralelos:

  • Si hay pocos archivos (la sobrecarga de paralelizar puede superar el beneficio).
  • Si se requiere un orden estricto o existen dependencias entre los elementos.

4. Alternativas y ajuste del paralelismo

¿Cuándo es mejor usar ExecutorService?

Los streams paralelos son buenos para casos sencillos. Pero si necesitas:

  • Controlar el número exacto de hilos (para tareas IO-bound suele convenir tener más hilos que núcleos).
  • Gestionar colas, cancelaciones, reintentos y manejo de errores.
  • Construir canalizaciones de tareas más complejas.

Entonces utiliza ExecutorService:

import java.nio.file.*;
import java.util.concurrent.*;

ExecutorService executor = Executors.newFixedThreadPool(8);
Files.walk(start)
    .filter(Files::isRegularFile)
    .forEach(path -> executor.submit(() -> processFile(path)));
executor.shutdown();

Ajuste de ForkJoinPool

Por defecto, el pool común usa un número de hilos igual al de procesadores. Puedes cambiarlo mediante una propiedad del sistema (antes del primer uso de streams paralelos):

System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "16");
  • Después de este ajuste, todos los streams paralelos usarán hasta 16 hilos.

CPU-bound vs IO-bound tareas

  • CPU-bound: cargan activamente la CPU (cálculo, parseo, compresión). Número de hilos ≈ número de núcleos.
  • IO-bound: mucho tiempo de espera de disco/red. A menudo conviene tener más hilos que núcleos.

Los streams paralelos no siempre son óptimos para tareas IO-bound — a menudo gana un ExecutorService propio con un pool ampliado.

5. Ejemplo: búsqueda y procesamiento paralelos de archivos

Calculemos el número total de líneas en todos los archivos ".java" del proyecto utilizando un recorrido paralelo.

import java.nio.file.*;
import java.util.stream.*;
import java.io.IOException;

public class LineCounter {
    public static void main(String[] args) throws IOException {
        Path start = Paths.get("src");

        long totalLines = Files.walk(start)
            .parallel() // ¡procesamiento paralelo!
            .filter(p -> p.toString().endsWith(".java"))
            .mapToLong(LineCounter::countLines)
            .sum();

        System.out.println("Total de líneas de código: " + totalLines);
    }

    // Método para contar líneas en un archivo
    private static long countLines(Path path) {
        try (Stream<String> lines = Files.lines(path)) {
            return lines.count();
        } catch (IOException e) {
            System.err.println("Error al leer el archivo: " + path);
            return 0;
        }
    }
}

Qué sucede:

  • Files.walk(start) — recorrido de todas las rutas.
  • parallel() — activamos el procesamiento paralelo.
  • filter(...) — dejamos solo los archivos ".java".
  • mapToLong(...) — contamos las líneas de cada archivo.
  • sum() — sumamos el resultado.

Ventajas: se aprovechan varios hilos y el código sigue siendo conciso.

6. Matices importantes y errores típicos

  • No todas las tareas se aceleran con paralelismo. Para conjuntos pequeños de archivos o operaciones rápidas, la sobrecarga puede ralentizar el programa.
  • Cierra los recursos. Al trabajar con archivos, usa try-with-resources — así no se filtrarán descriptores. Por ejemplo, Files.lines(path) en try(...).
  • Paralelismo anidado. Ejecutar streams paralelos dentro de otras tareas paralelas (nested parallelism) rara vez es eficaz y puede degradar el rendimiento.
  • Efectos secundarios. Evita escribir en estructuras/archivos compartidos sin sincronización. Prefiere operaciones «puras» sobre los elementos.

7. Esquema: cómo funciona el recorrido paralelo de archivos

flowchart TD
    A["Files.walk(start)"] --> B["Stream<Path>"]
    B --> C{".parallel()?"}
    C -- No --> D[forEach normal]
    C -- Sí --> E["forEach paralelo (ForkJoinPool)"]
    E --> F[Procesamiento de archivos en varios hilos]

8. Errores típicos en el procesamiento paralelo de archivos

Error n.º 1: Uso de streams paralelos para tareas pequeñas — la sobrecarga es mayor que el beneficio.

Error n.º 2: Esperar que los streams paralelos aceleren las tareas IO-bound igual que las CPU-bound. Para IO, a menudo se necesita ExecutorService con un pool más grande.

Error n.º 3: Excepciones no controladas en lambdas — sin manejar IOException el stream puede interrumpirse y el resultado quedar incompleto.

Error n.º 4: Condiciones de carrera al escribir en variables o archivos compartidos — sincroniza el acceso o evita los efectos secundarios.

Error n.º 5: Olvidar cerrar recursos — usa try-with-resources para todas las operaciones con archivos.

Error n.º 6: Intentar cambiar ForkJoinPool.commonPool() después del primer uso — la configuración mediante System.setProperty(...) debe hacerse de antemano.

Error n.º 7: Usar streams paralelos dentro de otros streams paralelos — a menudo lleva a una degradación del rendimiento.

1
Tarea
JAVA 25 SELF, nivel 59, lección 2
Bloqueada
Conteo total de archivos en un directorio y sus subdirectorios
Conteo total de archivos en un directorio y sus subdirectorios
1
Tarea
JAVA 25 SELF, nivel 59, lección 2
Bloqueada
Búsqueda paralela de archivos por fecha de modificación
Búsqueda paralela de archivos por fecha de modificación
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION