1. Introdução aos NIO Channels
No Java IO clássico (java.io), tudo funciona pelo princípio “uma thread — um arquivo ou recurso”. Assim que a leitura ou escrita começa, a thread é bloqueada e aguarda a conclusão da operação. Para casos simples isso é conveniente, mas em sistemas com alta carga essa abordagem vira um gargalo: se houver milhares de conexões, milhares de threads ficam ocupadas esperando.
No NIO (New IO) a abordagem é diferente. Aqui o I/O pode ser não bloqueante, e a thread não precisa ficar ociosa. Enquanto alguns dados ainda estão chegando, ela pode alternar para outra tarefa. Isso permite atender a uma quantidade enorme de conexões com apenas algumas threads.
A diferença também aparece nos detalhes. No IO “antigo”, o trabalho gira em torno de streams que leem e escrevem bytes ou caracteres, mas sempre bloqueiam durante as operações. No NIO, os conceitos centrais são os canais (Channels) e os buffers (Buffers). Eles permitem implementar I/O não bloqueante (importante para servidores), além de aplicar zero-copy, quando os dados são transferidos diretamente, evitando cópias desnecessárias para buffers da JVM.
Comparação: streams (Streams) vs canais (Channels)
Streams (InputStream/OutputStream):
- Leem/escrevem bytes um a um ou em arrays.
- Não há controle direto sobre a posição no arquivo.
- Não são eficientes com arquivos muito grandes.
Canais (Channel):
- Leem/escrevem dados via buffers (Buffer).
- Permitem controlar a posição (incluindo acesso randômico).
- Suportam assincronia e modo não bloqueante.
- Permitem usar zero-copy para cópias muito rápidas.
2. FileChannel e SeekableByteChannel
Leitura e escrita de dados usando buffers
FileChannel é o canal principal para trabalhar com arquivos. Ele pode ser obtido de FileInputStream, FileOutputStream ou via NIO.2 — Files.newByteChannel (retorna SeekableByteChannel).
Exemplo: leitura de arquivo com FileChannel e ByteBuffer
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelReadExample {
public static void main(String[] args) throws Exception {
try (RandomAccessFile file = new RandomAccessFile("data.txt", "r");
FileChannel channel = file.getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate(1024); // buffer de 1 KB
int bytesRead = channel.read(buffer); // lemos para o buffer
while (bytesRead != -1) {
buffer.flip(); // alternamos o buffer para o modo de leitura
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear(); // limpamos o buffer para a próxima leitura
bytesRead = channel.read(buffer);
}
}
}
}
Escrita em arquivo:
try (RandomAccessFile file = new RandomAccessFile("output.txt", "rw");
FileChannel channel = file.getChannel()) {
ByteBuffer buffer = ByteBuffer.wrap("Hello, NIO!\n".getBytes());
channel.write(buffer);
}
NIO.2: abrindo um canal via Files.newByteChannel
import java.nio.file.*;
import java.nio.channels.SeekableByteChannel;
import static java.nio.file.StandardOpenOption.*;
Path path = Paths.get("data.txt");
try (SeekableByteChannel ch = Files.newByteChannel(path, READ)) {
ByteBuffer buf = ByteBuffer.allocate(256);
ch.read(buf);
}
Posicionamento (position()) e truncamento (truncate())
- position() — permite obter ou definir a posição atual no arquivo (análogo a um “cursor”).
- truncate(long size) — corta o arquivo para o tamanho especificado.
channel.position(100); // mover para o byte 100
channel.truncate(1024); // truncar o arquivo para 1 KB
Acesso direto e posicional a arquivos
- Acesso direto: é possível ler/escrever em qualquer ponto do arquivo, não apenas de forma sequencial.
- Acesso posicional: é possível ler/escrever dados em uma posição específica sem alterar a posição atual do canal.
ByteBuffer buffer = ByteBuffer.allocate(4);
channel.read(buffer, 128); // ler 4 bytes a partir da posição 128, sem alterar channel.position()
3. ByteBuffer: como funciona
Parâmetros principais: capacity, limit, position, mark
- capacity — tamanho máximo do buffer (definido na criação).
- limit — limite até onde se pode ler/escrever (por padrão igual a capacity).
- position — posição atual (para onde escrevemos/de onde lemos).
- mark — uma “marca” que pode ser definida e, depois, retornada.
Ciclo de vida do buffer:
- Escrevemos dados no buffer (por exemplo, lendo do canal read()).
- flip() — alterna o buffer para o modo de leitura (position = 0, limit = valor atual de position).
- Lemos os dados do buffer (get()).
- clear() — limpa o buffer para a próxima escrita (position = 0, limit = capacity).
Exemplo:
ByteBuffer buffer = ByteBuffer.allocate(8);
buffer.put((byte) 42);
buffer.flip(); // agora é possível ler
byte value = buffer.get(); // 42
buffer.clear(); // pronto para nova escrita
Criação de buffers: allocate() vs allocateDirect()
O ByteBuffer tem duas maneiras principais de criar um buffer, e a diferença entre elas é perceptível na prática. O método allocate() aloca o buffer no heap da JVM: é criado rapidamente e serve para a maioria dos casos, mas em I/O nativo podem ocorrer cópias adicionais entre o heap e a memória do SO.
O método allocateDirect() aloca memória fora do heap da JVM (na “native memory”). Esse buffer é mais caro de criar e mais difícil de gerenciar, mas ao ler/gravar arquivos grandes ou em operações de rede costuma ser mais rápido por evitar cópias desnecessárias.
A ideia é simples: se o desempenho com grandes volumes é crítico — use buffers diretos. Para operações pequenas e frequentes, o custo de criá-los pode superar o benefício.
ByteBuffer directBuffer = ByteBuffer.allocateDirect(4096);
4. Operações de alto desempenho: transferTo() e transferFrom()
Métodos transferTo() e transferFrom()
A classe FileChannel tem dois métodos que permitem trabalhar no princípio de “cópia zero” — transferTo() e transferFrom(). A ideia é que os dados podem ser transferidos diretamente entre canais de arquivo ou, por exemplo, entre um arquivo e a rede. A JVM quase não participa: a operação é realizada pelo SO, e os buffers dentro do Java não são mexidos.
Como resultado, copiar arquivos grandes fica visivelmente mais rápido: menos cópias, menos comutações entre user space e kernel space e menor carga na CPU.
Exemplo: cópia de arquivo via zero-copy
import java.nio.channels.FileChannel;
import java.nio.file.*;
public class ZeroCopyExample {
public static void main(String[] args) throws Exception {
try (FileChannel src = FileChannel.open(Paths.get("input.bin"), StandardOpenOption.READ);
FileChannel dst = FileChannel.open(Paths.get("output.bin"), StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
long size = src.size();
long transferred = src.transferTo(0, size, dst);
System.out.println("Bytes copiados: " + transferred);
}
}
}
Quando o zero-copy realmente funciona?
- Ao copiar entre arquivos no mesmo disco.
- Ao enviar arquivos pela rede (por exemplo, via SocketChannel).
- Quando o SO oferece suporte a zero-copy (Linux, macOS, Windows — oferecem).
Vantagens:
- Mínimas cópias: os dados não passam pelos buffers da JVM.
- Alta velocidade: menos trocas de contexto e menor carga da CPU.
- Menos memória: não são necessários buffers grandes no lado do usuário.
Exemplo: cópia de arquivo “em uma única linha”
Files.copy(Paths.get("input.bin"), Paths.get("output.bin"), StandardCopyOption.REPLACE_EXISTING);
// Por baixo dos panos pode usar zero-copy, se possível
5. Erros comuns
Erro nº 1: esqueceu flip() antes de ler do buffer. Depois de escrever no buffer, chame flip(); caso contrário, a leitura não funcionará como esperado: position/limit permanecerão no “modo de escrita”.
Erro nº 2: usar allocateDirect() para operações pequenas. Buffers diretos são bons para grandes volumes, mas para requisições pequenas sua criação é injustificadamente cara. Por padrão, prefira allocate().
Erro nº 3: não fechar o canal. Sempre use try-with-resources com canais e streams para evitar vazamento de descritores.
Erro nº 4: confundir position/limit/capacity. Antes de ler/escrever, verifique em qual modo o buffer está: após a escrita é necessário flip(), após a leitura, para nova escrita — clear() ou compact().
Erro nº 5: esperar que o zero-copy “sempre funcione”. Em algumas configurações (dispositivos diferentes/sistemas de arquivos distintos/flags especiais), o zero-copy pode não estar disponível — nesse caso ocorrerá a cópia padrão, e o desempenho será diferente.
GO TO FULL VERSION