CodeGym /Cursos /JAVA 25 SELF /IO assíncrono: AsynchronousFileChannel (NIO2)

IO assíncrono: AsynchronousFileChannel (NIO2)

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

1. Introdução ao IO assíncrono

Vamos esclarecer os termos. No IO clássico (síncrono), quando você chama um método de leitura ou gravação, sua thread de execução (por exemplo, a thread principal do programa) para e espera até a operação terminar. É como ligar para um amigo e, até ele atender, você ficar parado olhando para o telefone.

IO assíncrono (AIO) — é quando você delega a operação de leitura/gravação ao sistema e continua trabalhando. Quando a operação terminar — vão “ligar” para você de volta (por exemplo, chamando seu método de callback ou retornando o resultado via Future).

Onde isso é útil?

  • Aplicações de servidor: para não desperdiçar threads enquanto o disco “pensa”.
  • Processamento em massa de arquivos grandes: para não bloquear a thread principal.
  • Aplicações com UI: para que a interface não “congele” durante leitura/gravação.

Imagine que você pediu uma pizza. No mundo síncrono, você ficaria na porta esperando o entregador. No assíncrono — você toca sua vida, e quando a pizza chegar, vão ligar e dizer: “Pizza aqui!”

2. Visão geral do AsynchronousFileChannel

No Java, a entrada/saída assíncrona é implementada no pacote java.nio.channels a partir da versão 7. O protagonista é a classe AsynchronousFileChannel.

O que ele faz?

  • Ler e gravar dados no arquivo de forma assíncrona.
  • Trabalhar com buffers (ByteBuffer).
  • Usar diferentes abordagens para obter o resultado: via Future ou via CompletionHandler.
  • Permite especificar explicitamente o pool de threads (ExecutorService) para processar eventos.

Métodos principais

  • read(ByteBuffer dst, long position): retorna Future<Integer>.
  • read(ByteBuffer dst, long position, A attachment, CompletionHandler<Integer, ? super A> handler).
  • write(ByteBuffer src, long position): retorna 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 o canal.

Formas de uso:

  • Via Future: você inicia a operação e depois pode aguardar sua conclusão.
  • Via CompletionHandler: você passa um “handler” que será chamado quando a operação terminar (ou falhar).

Exemplo de abertura de arquivo para leitura/gravação assíncrona

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

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

Você também pode especificar explicitamente o pool de threads para processar eventos:

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
);

Fato interessante:
Se você não especificar um ExecutorService, o Java criará seu próprio pool interno de threads para atender aos eventos de IO. Para tarefas simples isso é suficiente, mas para aplicações de servidor é melhor gerenciar o pool manualmente.

3. Executores (ExecutorService) e seu papel

Quando você trabalha com um canal assíncrono, em algum lugar nos bastidores o Java precisa executar seus callbacks ou concluir o Future. Isso não acontece por mágica: é feito por threads de trabalho especiais — o executor service.

Se você não fornecer seu próprio pool de threads, o Java simplesmente cria um interno — geralmente uma thread por processador. É conveniente, mas nem sempre ideal. Quando você quer controlar quantas threads rodam, quais tarefas são mais importantes e como a carga é distribuída, é melhor criar seu próprio ExecutorService e passá-lo para o open.

Em aplicações de servidor isso é especialmente importante. Sem um pool próprio, você pode ter picos inesperados de carga — e, em vez de um funcionamento suave, o servidor começa a engasgar.

Exemplo:

ExecutorService pool = Executors.newFixedThreadPool(8);

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

Impacto da escolha do pool:

  • Muitas threads — mais paralelismo, mas também mais carga no sistema.
  • Poucas threads — menos operações simultâneas, porém menos overhead.
  • Se você iniciar milhares de operações assíncronas, pense no equilíbrio!

4. Prática: leitura assíncrona de arquivo

Leitura síncrona (para comparação)

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

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

O problema é que a thread simplesmente espera até que o arquivo inteiro seja lido. Se o arquivo for grande ou o disco estiver lento, o programa também fica “lento” — todo o resto fica em pausa nesse momento.

Leitura assíncrona com AsynchronousFileChannel e 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); // ler 1 KB

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

            // Dá para fazer outras coisas em paralelo!
            System.out.println("Leitura iniciada...");

            // ... e depois aguardamos o resultado
            int bytesRead = result.get(); // bloqueia a thread até a operação terminar

            System.out.println("Bytes lidos: " + bytesRead);

            buffer.flip();
            // Converte os bytes em string (se for texto)
            byte[] data = new byte[bytesRead];
            buffer.get(data, 0, bytesRead);
            String text = new String(data);
            System.out.println("Conteúdo: " + text);
        }
    }
}
  • channel.read(buffer, 0) — inicia a leitura assíncrona a partir da posição 0.
  • Retorna um Future<Integer>, que pode ser usado para aguardar o resultado.
  • Enquanto a operação não termina, você pode executar outras ações.
  • result.get() bloqueia a thread, mas somente se o resultado ainda não estiver pronto.

Leitura assíncrona com CompletionHandler

(Vamos detalhar mais na próxima aula, mas para dar um gostinho...)

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("Lido de forma assíncrona: " + text);
                }

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

            // Não deixe o programa terminar imediatamente (senão o callback não roda)
            Thread.sleep(100); // Em aplicações reais — melhor sincronizar via latch, future etc.
        }
    }
}

5. Dicas úteis

Comparação: leitura assíncrona vs síncrona

Característica IO síncrono ( Files.readAllBytes ) IO assíncrono ( AsynchronousFileChannel )
Bloqueia a thread Sim Não (se não chamar get())
Escalabilidade Baixa Alta
Adequado para UI/servidores Não Sim
Complexidade do código Simples Um pouco mais complexo
Gerenciamento de recursos Simples É importante não esquecer de fechar o canal!

Diagrama do funcionamento do IO assíncrono

sequenceDiagram
    participant Main as Sua thread
    participant OS as Sistema operacional
    participant Disk as Disco

    Main->>OS: Inicia leitura assíncrona (read)
    OS->>Disk: Lê os dados
    Main->>Main: Executa outras tarefas
    OS-->>Main: Informa a conclusão (Future/CompletionHandler)
    Main->>Main: Processa o resultado

6. Erros comuns ao trabalhar com AsynchronousFileChannel

Erro nº 1: esqueceu de fechar o canal.
AsynchronousFileChannel é um recurso que precisa ser fechado. Se você esquecer de fechar o canal (channel.close() ou try-with-resources), pode haver vazamento de descritores e problemas de acesso a arquivos. Use try-with-resources sempre que possível.

Erro nº 2: get() bloqueante na thread principal.
Se você usa Future e chama get() na thread principal (por exemplo, em uma aplicação de UI), você perde o sentido do IO assíncrono — a thread vai esperar do mesmo jeito. Use CompletionHandler ou uma thread separada para aguardar o resultado.

Erro nº 3: uso incorreto de ByteBuffer.
Após gravar no buffer, não se esqueça de chamar flip() para prepará-lo para leitura. Após ler — clear() ou compact(), se for reutilizá-lo.

Erro nº 4: esquecer de tratar erros.
Operações assíncronas podem terminar com erro (por exemplo, arquivo não encontrado, sem permissão). Se você não tratar as exceções no CompletionHandler ou não verificar o Future, a operação pode falhar silenciosamente.

Erro nº 5: paralelismo não considerado.
Se você inicia várias operações no mesmo canal simultaneamente, garanta que seu código seja thread-safe e que não ocorram disputas por buffers ou posições do arquivo.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION