CodeGym /Cursos /JAVA 25 SELF /E/S asíncrona: AsynchronousFileChannel (NIO2)

E/S asíncrona: AsynchronousFileChannel (NIO2)

JAVA 25 SELF
Nivel 56, Lección 0
Disponible

1. Introducción a la E/S asíncrona

Aclaremos los términos de inmediato. En la E/S clásica (síncrona), cuando llamas a un método de lectura o escritura, tu hilo de ejecución (por ejemplo, el hilo principal del programa) se detiene y espera a que la operación termine. Es como si llamaras a un amigo y, hasta que no descuelgue, te quedaras sin hacer nada mirando el teléfono.

La E/S asíncrona (AIO) es cuando encargas la operación de lectura/escritura al sistema y tú sigues trabajando. Cuando la operación termina, te «llaman» de vuelta (por ejemplo, se invoca tu método de callback o te devuelven el resultado a través de un Future).

¿Dónde se necesita?

  • Aplicaciones de servidor: para no gastar hilos en vano mientras el disco «piensa».
  • Procesamiento masivo de archivos grandes: para no bloquear el hilo principal.
  • Aplicaciones con UI: para que la interfaz no «se congele» durante la lectura/escritura.

Imagina que has pedido una pizza. En el mundo síncrono estarías de pie en la puerta esperando al repartidor. En el asíncrono, haces tus cosas y, cuando llegue la pizza, te llamarán y te dirán: «¡La pizza está aquí!»

2. Visión general de AsynchronousFileChannel

En Java, la E/S asíncrona está implementada en el paquete java.nio.channels desde la versión 7. El protagonista es la clase AsynchronousFileChannel.

¿Qué puede hacer?

  • Leer y escribir datos en un archivo de manera asíncrona.
  • Trabajar con búferes (ByteBuffer).
  • Usar diferentes enfoques para obtener el resultado: mediante Future o mediante CompletionHandler.
  • Permite especificar explícitamente un pool de hilos (ExecutorService) para procesar eventos.

Métodos principales

  • read(ByteBuffer dst, long position): devuelve Future<Integer>.
  • read(ByteBuffer dst, long position, A attachment, CompletionHandler<Integer, ? super A> handler).
  • write(ByteBuffer src, long position): devuelve Future<Integer>.
  • write(ByteBuffer src, long position, A attachment, CompletionHandler<Integer, ? super A> handler).
  • static open(Path file, Set<OpenOption> options, ExecutorService executor, FileAttribute<?>... attrs) — abre el canal.

Modos de uso:

  • A través de Future: lanzas la operación y luego puedes esperar a que termine.
  • A través de CompletionHandler: pasas un «manejador» que se invocará cuando la operación finalice (o falle con un error).

Ejemplo de apertura de archivo para lectura/escritura asíncrona

import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

AsynchronousFileChannel channel = AsynchronousFileChannel.open(
    Path.of("data.txt"),
    StandardOpenOption.READ, StandardOpenOption.WRITE
);

También se puede especificar explícitamente el pool de hilos para procesar eventos:

import java.util.EnumSet;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

ExecutorService executor = Executors.newFixedThreadPool(4);

AsynchronousFileChannel channel = AsynchronousFileChannel.open(
    Path.of("data.txt"),
    EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE),
    executor
);

Dato interesante:
Si no especificas un ExecutorService, Java creará su propio pool interno de hilos para atender los eventos de E/S. Para tareas sencillas es suficiente, pero para aplicaciones de servidor es mejor gestionar el pool manualmente.

3. Pools de ejecución (ExecutorService) y su papel

Cuando trabajas con un canal asíncrono, en algún lugar detrás del telón Java debe ejecutar tus callbacks o completar el Future. No lo hace por arte de magia, sino con hilos de trabajo especiales — un executor service.

Si no pasas tu propio pool de hilos, Java simplemente crea uno interno — normalmente un hilo por procesador. Cómodo, pero no siempre seguro. Cuando quieres controlar cuántos hilos se ejecutan, qué tareas son más importantes y cómo se reparte la carga, es mejor crear tu propio ExecutorService y pasarlo a open.

En aplicaciones de servidor esto es especialmente importante. Sin un pool propio puedes sufrir picos de carga inesperados — y, en lugar de un funcionamiento fluido, el servidor empieza a ahogarse.

Ejemplo:

ExecutorService pool = Executors.newFixedThreadPool(8);

AsynchronousFileChannel channel = AsynchronousFileChannel.open(
    Path.of("huge.log"),
    EnumSet.of(StandardOpenOption.READ),
    pool
);

Impacto de la elección del pool:

  • Muchos hilos — más paralelismo, pero también más carga en el sistema.
  • Pocos hilos — menos operaciones simultáneas, pero menor sobrecarga.
  • Si lanzas miles de operaciones asíncronas, ¡piensa en el equilibrio!

4. Práctica: lectura asíncrona de archivos

Lectura síncrona (para comparar)

import java.nio.file.Files;
import java.nio.file.Path;

byte[] data = Files.readAllBytes(Path.of("input.txt"));
System.out.println("Bytes leídos: " + data.length);

El problema aquí es que el hilo simplemente espera hasta que se lea todo el archivo. Si el archivo es grande o el disco es lento, el programa también empieza a «ir lento» — todo lo demás queda en pausa en ese momento.

Lectura asíncrona con AsynchronousFileChannel y Future

import java.nio.channels.AsynchronousFileChannel;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;

public class AsyncReadExample {
    public static void main(String[] args) throws Exception {
        Path path = Path.of("input.txt");
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocate(1024); // leemos de 1 KB

            Future<Integer> result = channel.read(buffer, 0);

            // ¡Se puede hacer algo más en paralelo!
            System.out.println("Lectura iniciada...");

            // ... y luego esperamos el resultado
            int bytesRead = result.get(); // bloquea el hilo hasta que termine la operación

            System.out.println("Bytes leídos: " + bytesRead);

            buffer.flip();
            // Convertimos bytes a cadena (si es texto)
            byte[] data = new byte[bytesRead];
            buffer.get(data, 0, bytesRead);
            String text = new String(data);
            System.out.println("Contenido: " + text);
        }
    }
}
  • channel.read(buffer, 0) — inicia la lectura asíncrona desde la posición 0.
  • Devuelve un Future<Integer> que se puede usar para esperar el resultado.
  • Mientras la operación no ha terminado, puedes realizar otras acciones.
  • result.get() bloquea el hilo, pero solo si el resultado aún no está listo.

Lectura asíncrona con CompletionHandler

(Lo veremos con más detalle en la próxima lección, pero como adelanto...)

import java.nio.channels.AsynchronousFileChannel;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.channels.CompletionHandler;

public class AsyncReadWithHandler {
    public static void main(String[] args) throws Exception {
        Path path = Path.of("input.txt");
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocate(1024);

            channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer bytesRead, ByteBuffer buf) {
                    buf.flip();
                    byte[] data = new byte[bytesRead];
                    buf.get(data, 0, bytesRead);
                    String text = new String(data);
                    System.out.println("Leído de forma asíncrona: " + text);
                }

                @Override
                public void failed(Throwable exc, ByteBuffer buf) {
                    System.err.println("Error de lectura: " + exc.getMessage());
                }
            });

            // Evitamos que el programa termine inmediatamente (de lo contrario no llegará el callback)
            Thread.sleep(100); // En aplicaciones reales — mejor sincronización con latch, future, etc.
        }
    }
}

5. Detalles útiles

Comparativa: lectura asíncrona vs. síncrona

Característica E/S síncrona ( Files.readAllBytes ) E/S asíncrona ( AsynchronousFileChannel )
Bloquea el hilo Sí No (si no llamas a get())
Escalabilidad Baja Alta
Adecuado para UI/servidores No Sí
Complejidad del código Simple Un poco más complejo
Gestión de recursos Simple ¡Es importante no olvidar cerrar el canal!

Esquema de trabajo de la E/S asíncrona

sequenceDiagram
    participant Main as Tu hilo
    participant OS as Sistema operativo
    participant Disk as Disco

    Main->>OS: Inicia lectura asíncrona (read)
    OS->>Disk: Lee los datos
    Main->>Main: Ejecuta otras tareas
    OS-->>Main: Notifica la finalización (Future/CompletionHandler)
    Main->>Main: Procesa el resultado

6. Errores típicos al trabajar con AsynchronousFileChannel

Error n.º 1: olvidaste cerrar el canal.
AsynchronousFileChannel es un recurso que debe cerrarse. Si olvidas cerrar el canal (channel.close() o try-with-resources), puedes tener fugas de descriptores y problemas de acceso a archivos. Usa try-with-resources siempre que sea posible.

Error n.º 2: get() bloqueante en el hilo principal.
Si usas Future y llamas a get() en el hilo principal (por ejemplo, en una aplicación con UI), pierdes el sentido de la E/S asíncrona: el hilo igualmente esperará. Usa CompletionHandler o un hilo aparte para esperar el resultado.

Error n.º 3: uso incorrecto de ByteBuffer.
Después de escribir en el búfer, no olvides hacer flip() para prepararlo para la lectura. Después de leer — clear() o compact() si lo vas a reutilizar.

Error n.º 4: olvidaste gestionar los errores.
Las operaciones asíncronas pueden terminar con error (por ejemplo, archivo no encontrado, sin permisos). Si no gestionas las excepciones en CompletionHandler o no compruebas el Future por error, el programa «en silencio» no realizará la operación.

Error n.º 5: paralelismo no considerado.
Si lanzas varias operaciones sobre el mismo canal al mismo tiempo, asegúrate de que tu código sea seguro para hilos y no haya carreras por los búferes o las posiciones del archivo.

1
Tarea
JAVA 25 SELF, nivel 56, lección 0
Bloqueada
Preparación para la recepción asíncrona de datos 🚀
Preparación para la recepción asíncrona de datos 🚀
1
Tarea
JAVA 25 SELF, nivel 56, lección 0
Bloqueada
Registro asíncrono de un evento importante en el registro ✍️
Registro asíncrono de un evento importante en el registro ✍️
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION