CodeGym /Cursos /JAVA 25 SELF /Análise de erros ao trabalhar com arquivos de forma assín...

Análise de erros ao trabalhar com arquivos de forma assíncrona

JAVA 25 SELF
Nível 56, Lição 4
Disponível

1. Erros com buffers: ByteBuffer, posição e limite

Os métodos assíncronos de leitura e escrita trabalham com um objeto ByteBuffer. Diferente de arrays comuns, o buffer possui um “cursor interno” — posição (position) e limite (limit) — que determinam quais bytes serão lidos ou escritos. Se você não gerenciar essas propriedades corretamente, pode obter um resultado diferente do esperado ou até quebrar a lógica.

Como isso fica no código?

Exemplo de uso incorreto:

ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
    @Override
    public void completed(Integer result, ByteBuffer buf) {
        // Opa! Tentando ler a string diretamente do buffer:
        String str = new String(buf.array()); // Isso está errado!
        // ...
    }
    // ...
});

O que há de errado?

  • Após a leitura, o buffer está no modo “escrita”: position aponta para o fim dos dados lidos e limit — para o tamanho do buffer. Se você ler do buffer imediatamente, obterá lixo (todos os 1024 bytes, mesmo que apenas 10 tenham sido realmente lidos).
  • A chamada buf.array() retorna todo o array interno, e não apenas a parte lida. Além disso, para buffers diretos (allocated via ByteBuffer.allocateDirect) array() lançará UnsupportedOperationException.

Como fazer corretamente?

Antes de ler os dados do buffer, chame buffer.flip() — isso alterna o buffer para o modo “leitura”:

public void completed(Integer result, ByteBuffer buf) {
    buf.flip(); // Agora position = 0, limit = quantidade de bytes lidos
    String str = StandardCharsets.UTF_8.decode(buf).toString();
    // ... processamento da string
}

Reutilização do buffer

Se você quiser reutilizar o buffer para a próxima operação, não se esqueça de chamar buffer.clear() ou buffer.compact() após processar os dados:

  • clear() — redefine completamente os limites: position=0, limit=capacity(), os dados antigos são considerados lixo.
  • compact() — preserva os bytes não lidos no início do buffer e o prepara para novas gravações.

Observação: se result for -1, o fim do arquivo foi alcançado — não haverá processamento adicional.

2. Acesso paralelo: condições de corrida e inconsistência

AsynchronousFileChannel permite iniciar várias operações em paralelo. Mas se você não controlar esse processo, é fácil obter dados corrompidos ou uma queda do programa.

Problema 1: Leituras simultâneas no mesmo buffer

// Duas leituras simultâneas no mesmo buffer
channel.read(buffer, 0, buffer, handler1);
channel.read(buffer, 1024, buffer, handler2);

Ambas as leituras gravam no mesmo buffer! Se ambas terminarem quase ao mesmo tempo, o conteúdo do buffer será imprevisível.

Problema 2: Escrita simultânea no mesmo arquivo

Se duas threads escreverem ao mesmo tempo na mesma região do arquivo, o resultado dependerá de qual operação terminar primeiro. Esta é uma race condition clássica, que pode levar a dados corrompidos.

Como evitar?

  • Para cada operação assíncrona, utilize um buffer (ByteBuffer) separado — as threads não irão “invadir” a memória uma da outra.
  • Não dispare gravações paralelas para a mesma faixa do arquivo; particione os offsets ou sincronize o acesso.
  • Se a ordem estrita for importante, inicie a nova operação apenas após a conclusão da anterior — por exemplo, a partir de completed(...) em CompletionHandler.

3. Vazamento de recursos: esquecer de fechar o canal

Um canal assíncrono é um recurso do sistema. Se você não o fechar (channel.close()), o arquivo permanecerá “ocupado” no sistema, podem ocorrer vazamentos de memória e, no Windows, o arquivo pode ainda ficar bloqueado para outros programas.

Erro típico:

AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, ...);
// ... operação iniciada
// esquecemos de chamar channel.close() após a conclusão de todas as operações!

Como fazer corretamente?

Utilize try-with-resources e certifique-se de esperar a conclusão de todas as operações antes de sair do bloco:

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) {
            // processamento...
            latch.countDown();
        }
        @Override public void failed(Throwable ex, ByteBuffer b) {
            ex.printStackTrace();
            latch.countDown();
        }
    });

    latch.await(); // Aguardamos a conclusão da operação assíncrona
}
// O fechamento ocorrerá automaticamente

4. Tratamento de exceções: ignorar erros em CompletionHandler

No código assíncrono, os erros não “explodem” na thread principal — eles vão para o método failed(...) da interface CompletionHandler. Se você não o implementar ou deixá-lo vazio, os erros simplesmente desaparecerão e o programa terá um comportamento estranho.

Exemplo de erro “invisível”:

channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
    @Override
    public void completed(Integer result, ByteBuffer buf) {
        // ... processamento do resultado
    }
    @Override
    public void failed(Throwable exc, ByteBuffer buf) {
        // Opa, vazio! O erro se perdeu
    }
});

Como fazer corretamente?

@Override
public void failed(Throwable exc, ByteBuffer buf) {
    System.err.println("Erro ao ler o arquivo: " + exc.getMessage());
    exc.printStackTrace();
    // Possivelmente: fechar o canal, atualizar métricas, notificar o usuário etc.
}

5. Perda de referências a Future/CompletionHandler

Se você iniciou uma operação assíncrona via Future, mas se esqueceu de armazenar a referência, não poderá cancelar a operação nem aguardar sua conclusão. Da mesma forma, se usar CompletionHandler, mas não sincronizar a conclusão de todas as operações, o programa pode terminar antes da hora.

Exemplo:

channel.read(buffer, 0, buffer, handler); // handler — anônimo, não é armazenado em lugar nenhum
// O programa terminou imediatamente, sem aguardar o fim da leitura

Como fazer corretamente?

  • Ao trabalhar com Future<Integer>: guarde a referência e use future.get() ou future.cancel(true) quando necessário.
  • Ao usar CompletionHandler: utilize mecanismos de sincronização (CountDownLatch, Semaphore) e aguarde corretamente a conclusão de todas as operações antes de fechar o canal/programa.

6. Erros com codificações de caracteres

A leitura e a escrita de arquivos de texto exigem o uso correto de codificações de caracteres. Se você converter bytes em strings “na marra”, pode obter caracteres ilegíveis ou perder parte dos dados, especialmente ao ler o arquivo em partes.

Problema:

// Lemos o arquivo em blocos de 1024 bytes e depois convertemos em string
String chunk = new String(buffer.array(), "UTF-8");

Se um caractere ficar dividido entre dois buffers (por exemplo, um byte de um símbolo UTF-8 ficou no fim de um buffer e os restantes — no início do seguinte), você obterá caracteres incorretos ou um erro de decodificação.

Como fazer corretamente? Use CharsetDecoder e armazene os “restos” de caracteres incompletos entre leituras:

CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
        .onMalformedInput(CodingErrorAction.REPORT)
        .onUnmappableCharacter(CodingErrorAction.REPORT);

ByteBuffer byteBuf = ByteBuffer.allocate(4096);
CharBuffer charBuf = CharBuffer.allocate(4096);

// Em cada completed(...):
byteBuf.flip();
CoderResult cr = decoder.decode(byteBuf, charBuf, false); // false — não é o fim da entrada
if (cr.isError()) {
    cr.throwException();
}
byteBuf.compact();
charBuf.flip();
String text = charBuf.toString();
charBuf.clear();

// Quando não houver mais entrada:
decoder.flush(charBuf);

7. Encerramento prematuro do programa

Operações assíncronas são executadas em outras threads. Se a thread principal terminar antes de todas as operações, o programa será encerrado sem aguardar o resultado.

Exemplo:

// Iniciamos a leitura assíncrona
channel.read(buffer, 0, buffer, handler);
// A thread principal terminou imediatamente — o programa fechou e a operação não teve tempo de terminar

Como fazer corretamente?

Use CountDownLatch, Semaphore ou ao menos Thread.sleep(...) (para demonstrações) para aguardar a conclusão de todas as operações:

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(); // Aguardamos a conclusão da operação

8. Integração malsucedida com ExecutorService

AsynchronousFileChannel permite especificar seu próprio ExecutorService para processar eventos. Se você fornecer um pool com poucos threads ou apenas um, todas as operações serão executadas de forma sequencial, e não paralela. Se o pool for grande demais — você terá sobrecarga desnecessária com troca de contexto.

Exemplo:

ExecutorService executor = Executors.newSingleThreadExecutor();
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, options, executor);
// Todas as operações assíncronas serão, na prática, síncronas!

Como fazer corretamente?

  • Ajuste o tamanho do pool de acordo com a carga real e o número de operações simultâneas.
  • Para a maioria dos casos, ForkJoinPool.commonPool() ou Executors.newCachedThreadPool() são suficientes.
  • Lembre-se de que o ExecutorService fornecido gerencia as chamadas dos callbacks (completed/failed), e não o próprio I/O de disco.
1
Tarefa
JAVA 25 SELF, nível 56, lição 4
Bloqueado
Envio de mensagem instantânea: escrita assíncrona com fechamento seguro ✉️
Envio de mensagem instantânea: escrita assíncrona com fechamento seguro ✉️
1
Tarefa
JAVA 25 SELF, nível 56, lição 4
Bloqueado
Sincronização da inicialização do sistema: aguardando o carregamento da configuração ⚙️
Sincronização da inicialização do sistema: aguardando o carregamento da configuração ⚙️
1
Pesquisa/teste
Operações assíncronas com arquivos, nível 56, lição 4
Indisponível
Operações assíncronas com arquivos
Operações assíncronas com arquivos
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION