1. Errores con los buffers: ByteBuffer, posición y límite
Los métodos asíncronos de lectura y escritura trabajan con un objeto ByteBuffer. A diferencia de los arrays normales, un buffer tiene un «cursor interno» — posición (position) y límite (limit) — que determinan qué bytes se leerán o escribirán. Si gestionas mal estas propiedades, puedes obtener un resultado distinto del esperado o incluso romper la lógica.
¿Cómo se ve en el código?
Ejemplo de uso incorrecto:
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buf) {
// ¡Ups! Intentamos leer la cadena directamente del buffer:
String str = new String(buf.array()); // ¡Esto es incorrecto!
// ...
}
// ...
});
¿Qué está mal?
- Tras la lectura, el buffer está en modo de «escritura»: position apunta al final de los datos leídos y limit al tamaño del buffer. Si lees del buffer inmediatamente, obtendrás basura (los 1024 bytes, aunque en realidad solo se hayan leído 10).
- La llamada a buf.array() devuelve todo el array interno, no solo la parte leída. Además, para buffers directos (asignados mediante ByteBuffer.allocateDirect) array() lanzará UnsupportedOperationException.
¿Cómo hacerlo correctamente?
Antes de leer datos del buffer, hay que llamar a buffer.flip() — esto pone el buffer en modo de «lectura»:
public void completed(Integer result, ByteBuffer buf) {
buf.flip(); // Ahora position = 0, limit = número de bytes leídos
String str = StandardCharsets.UTF_8.decode(buf).toString();
// ... procesamiento de la cadena
}
Reutilización del buffer
Si deseas reutilizar el buffer para la siguiente operación, no olvides llamar a buffer.clear() o buffer.compact() después de procesar los datos:
- clear() — restablece por completo los límites: position=0, limit=capacity(); los datos antiguos se consideran basura.
- compact() — conserva los bytes no leídos al principio del buffer y lo prepara para seguir escribiendo.
Matiz: si result es igual a -1, se ha alcanzado el fin de archivo — no habrá procesamiento adicional.
2. Acceso paralelo: condiciones de carrera e inconsistencia
AsynchronousFileChannel permite lanzar varias operaciones en paralelo. Pero si no controlas este proceso, es fácil obtener datos «corruptos» o que el programa falle.
Problema 1: Lectura simultánea en un mismo buffer
// Dos lecturas simultáneas en el mismo buffer
channel.read(buffer, 0, buffer, handler1);
channel.read(buffer, 1024, buffer, handler2);
¡Ambas lecturas escriben en el mismo buffer! Si terminan casi al mismo tiempo, el contenido del buffer será impredecible.
Problema 2: Escritura simultánea en un mismo archivo
Si dos hilos escriben a la vez en la misma zona del archivo, el resultado depende de qué operación termine antes. Es una condición de carrera (race condition) clásica que puede provocar datos dañados.
¿Cómo evitarlo?
- Para cada operación asíncrona utiliza un buffer independiente (ByteBuffer) — así los hilos no «pisarán» la memoria de otros.
- No inicies escrituras paralelas sobre el mismo rango del archivo; separa los desplazamientos (offsets) o sincroniza el acceso.
- Si importa un orden estricto, inicia la nueva operación solo después de que termine la anterior — por ejemplo, desde completed(...) de CompletionHandler.
3. Fugas de recursos: se olvidó cerrar el canal
Un canal asíncrono es un recurso del sistema. Si no se cierra (channel.close()), el archivo permanecerá «ocupado» en el sistema, pueden aparecer fugas de memoria y, en Windows, además se bloqueará para otros programas.
Error típico:
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, ...);
// ... se lanzó la operación
// ¡Se olvidó llamar a channel.close() tras finalizar todas las operaciones!
¿Cómo hacerlo correctamente?
Utiliza try-with-resources y asegúrate de esperar a que terminen todas las operaciones antes de salir del bloque:
CountDownLatch latch = new CountDownLatch(1);
try (AsynchronousFileChannel channel =
AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buf = ByteBuffer.allocate(4096);
channel.read(buf, 0, buf, new CompletionHandler<Integer, ByteBuffer>() {
@Override public void completed(Integer r, ByteBuffer b) {
// procesamiento...
latch.countDown();
}
@Override public void failed(Throwable ex, ByteBuffer b) {
ex.printStackTrace();
latch.countDown();
}
});
latch.await(); // Esperamos a que termine la operación asíncrona
}
// El cierre ocurrirá automáticamente
4. Manejo de excepciones: ignorar errores en CompletionHandler
En el código asíncrono, los errores no «saltan» en el hilo principal — llegan al método failed(...) de la interfaz CompletionHandler. Si no lo implementas o lo dejas vacío, los errores simplemente se perderán y el programa se comportará de forma extraña.
Ejemplo de error «invisible»:
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buf) {
// ... procesamiento del resultado
}
@Override
public void failed(Throwable exc, ByteBuffer buf) {
// Ups, ¡vacío! Se perdió el error
}
});
¿Cómo hacerlo correctamente?
@Override
public void failed(Throwable exc, ByteBuffer buf) {
System.err.println("Error al leer el archivo: " + exc.getMessage());
exc.printStackTrace();
// Posible: cerrar el canal, actualizar métricas, notificar al usuario, etc.
}
5. Pérdida de referencias a Future/CompletionHandler
Si iniciaste una operación asíncrona mediante Future pero olvidaste guardar la referencia, no podrás cancelar la operación ni esperar a que termine. De forma similar, si usas CompletionHandler pero no sincronizas la finalización de todas las operaciones, el programa puede terminar antes de tiempo.
Ejemplo:
channel.read(buffer, 0, buffer, handler); // handler: anónimo, no se guarda en ningún sitio
// El programa terminó de inmediato sin esperar a que acabase la lectura
¿Cómo hacerlo correctamente?
- Al trabajar con Future<Integer>: guarda la referencia y utiliza future.get() o future.cancel(true) si es necesario.
- Al usar CompletionHandler: aplica mecanismos de sincronización (CountDownLatch, Semaphore) y espera correctamente a que todas las operaciones finalicen antes de cerrar el canal o el programa.
6. Errores con las codificaciones
La lectura y escritura de archivos de texto requieren un manejo correcto de las codificaciones. Si conviertes bytes a cadenas «a lo bruto», puedes obtener caracteres ilegibles o perder datos, sobre todo al leer el archivo por partes.
Problema:
// Leemos el archivo por bloques de 1024 bytes y luego lo convertimos a cadena
String chunk = new String(buffer.array(), "UTF-8");
Si una cadena se divide entre dos buffers (por ejemplo, un byte de un carácter UTF-8 queda al final de un buffer y el resto al principio del siguiente), obtendrás caracteres incorrectos o un error de decodificación.
¿Cómo hacerlo correctamente? Utiliza CharsetDecoder y mantén los «restos» de caracteres incompletos entre lecturas:
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT);
ByteBuffer byteBuf = ByteBuffer.allocate(4096);
CharBuffer charBuf = CharBuffer.allocate(4096);
// En cada completed(...):
byteBuf.flip();
CoderResult cr = decoder.decode(byteBuf, charBuf, false); // false — no es el fin de la entrada
if (cr.isError()) {
cr.throwException();
}
byteBuf.compact();
charBuf.flip();
String text = charBuf.toString();
charBuf.clear();
// Cuando ya no vaya a haber más entrada:
decoder.flush(charBuf);
7. Finalización prematura del programa
Las operaciones asíncronas se ejecutan en otros hilos. Si el hilo principal termina antes de que terminen todas las operaciones, el programa finalizará sin esperar los resultados.
Ejemplo:
// Iniciamos la lectura asíncrona
channel.read(buffer, 0, buffer, handler);
// El hilo principal terminó enseguida — el programa se cerró y la operación no alcanzó a finalizar
¿Cómo hacerlo correctamente?
Utiliza CountDownLatch, Semaphore o, al menos, Thread.sleep(...) (para demos), para esperar a que todas las operaciones terminen:
CountDownLatch latch = new CountDownLatch(1);
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override public void completed(Integer result, ByteBuffer buf) { /* ... */ latch.countDown(); }
@Override public void failed(Throwable exc, ByteBuffer buf) { /* ... */ latch.countDown(); }
});
latch.await(); // Esperamos a que finalice la operación
8. Mala integración con ExecutorService
AsynchronousFileChannel permite proporcionar tu propio ExecutorService para procesar eventos. Si pasas un pool con pocos hilos o incluso de un solo hilo, todas las operaciones se ejecutarán secuencialmente, no en paralelo. Si el pool es demasiado grande, tendrás sobrecoste innecesario por cambios de contexto.
Ejemplo:
ExecutorService executor = Executors.newSingleThreadExecutor();
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, options, executor);
// ¡Todas las operaciones asíncronas serán, en esencia, sincrónicas!
¿Cómo hacerlo correctamente?
- Ajusta el tamaño del pool según la carga real y el número de operaciones concurrentes.
- Para la mayoría de los casos son adecuados ForkJoinPool.commonPool() o Executors.newCachedThreadPool().
- Recuerda que el ExecutorService proporcionado gestiona las invocaciones de los callbacks (completed/failed), no el propio I/O de disco.
GO TO FULL VERSION