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