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.
GO TO FULL VERSION