Esse artigo faz parte da revista Java Magazine edição 10. Clique aqui para ler todos os artigos desta edição

 

 

Atenção: por essa edição ser muito antiga não há arquivo PDF para download.Os artigos dessa edição estão disponíveis somente através do formato HTML. 

 

Pente fino

New I/O Fundamental

Parte 2: Operações Avançadas

Arquivos mapeados em memória, I/O não-bloqueante, locking de arquivos e multiplexação – o estado-da-arte em I/O com Java

No artigo anterior apresentamos os conceitos básicos do NIO e algumas novas operações oferecidas por essa API, como a transferência direta entre buffers e buffers diretos. Neste segundo artigo, apresentaremos recursos avançados da API New I/O que colocam a linguagem Java em pé de igualdade com linguagens de mais baixo nível como C/C++, na área de manipulação de I/O: mapeamento de arquivos em memória, file locking, I/O não-bloqueante e multiplexação e I/O.

Mapeamento de arquivo em memória

Quando um arquivo é aberto por um processo, o sistema operacional (SO) cria um buffer na memória virtual para a transferência de dados entre o processo e o arquivo. Ou seja, em vez de escrever diretamente no arquivo, o processo escreve em um buffer e o SO, de tempos em tempos, transfere o conteúdo do buffer para o arquivo (a leitura é feita de forma similar, usando também um buffer). Embora seja um procedimento transparente ao programador, a transferência para buffers intermediários penaliza a performance em algumas situações, principalmente quando o arquivo manipulado é muito grande ou acessado por vários processos.

Para resolver esse problema, sistemas operacionais modernos oferecem a possibilidade de mapear um arquivo diretamente em memória (memory-mapped files). A principal vantagem em relação ao procedimento tradicional é que o SO não precisa alocar memória física para o arquivo –­ além da economia de memória, ganha-se em performance, pois os acessos à memória mapeada nunca geram page faults.[1]

A economia de memória é ainda maior quando vários processos mapeiam o mesmo arquivo. Pelo procedimento tradicional, seria preciso alocar uma região de memória física para cada processo acessando o arquivo. Além disso, as modificações realizadas na memória mapeada são imediatamente refletidas no arquivo e vice-versa.

Para mapear um arquivo em memória com NIO, basta chamar map(FileChannel.MapMode modo, long pos, long tamanho) em um FileChannel associado ao arquivo, e será retornado um objeto java.nio.MappedByteBuffer. Os parâmetros pos e tamanho definem o trecho do arquivo a ser mapeado, e modo, a maneira como o mapeamento será feito.

O exemplo da Listagem 1 compara o consumo de memória usando o mapeamento direto e a alocação de buffers, e a Listagem 2 mostra o resultado para a um arquivo de 15 MB – note que a quantia de memória alocada para a JVM não se altera quando é usado mapeamento em memória, mas sim apenas após a alocação de um buffer.

A aplicação JCanyon (veja links) é um exemplo interessante do uso de NIO um simulador de vôo que usa o mapeamento direto em memória para ler as imagens de satélite (que ocupam cerca de 200 Mb) e dados topográficos (mais de 100 Mb), além de buffers diretos para o envio de informações à placa de vídeo.

File Locking

Imagine a situação em que vários threads precisem ler e alterar um mesmo arquivo. É preciso um mecanismo que garanta que, enquanto um thread altera o arquivo, os demais não possam acessá-lo. Como os threads estão sendo executados na mesma JVM, bastaria isolar os trechos que fazem acesso ao arquivo em blocos synchronized, para que o próprio sistema de threads da JVM resolvesse o problema. Mas e se o arquivo fosse acessado por threads executando em JVMs diferentes, ou mesmo por programas escritos em outras linguagens de programação?

Nessas situações, a saída é delegar ao sistema operacional o controle de acesso ao arquivo, através do mecanismo de file locking, que em NIO é implementado pelos métodos lock() e tryLock() da classe FileChannel. O primeiro método bloqueia o thread atual até que ela obtenha o lock no arquivo (representado pela classe java.nio.channels.FileLock); o segundo não bloqueia o thread, mas retorna null caso não tenha sido possível obter o lock (veja mais sobre o conceito de bloqueio na seção "I/O não bloqueante", a seguir).

Também é possível obter um lock apenas para um trecho de um arquivo, através dos métodos sobrecarregados lock(int posInicial, int tamanho, boolean compartilhado) e tryLock(int posInicial, int tamanho, boolean compartilhado). Aqui, o terceiro parâmetro determina se o lock será compartilhado ou exclusivo – quando um trecho do arquivo é bloqueado em modo compartilhado, esse trecho pode ser bloqueado por outros programas que também adquiram um lock em modo compartilhado, mas não em modo exclusivo. Tipicamente, o lock compartilhado é usado para leitura, e o modo exclusivo, para escrita.

A Listagem 3 mostra um exemplo de como fazer o locking em um arquivo inteiro; a Listagem 4 e a Tabela 1 apresentam o resultado de várias aplicações tentando obter o lock simultaneamente. Note que a API ainda não fornece um método para realizar o locking compartilhado de um arquivo inteiro: é preciso simular esse método chamando lock(0, Long.MAX_VALUE, true).

Por fim, como o mecanismo de file locking é de responsabilidade do SO e não da JVM, é importante salientar alguns pontos:

·         Nem todos os SOs suportam locks compartilhados. Para saber se o lock obtido é compartilhado ou não, chame o método isShared() de FileLock;

·         Locks podem ser implementados de duas formas pelos SOs: forçados (mandatory) e recomendados (advisory). Quando são recomendados, o SO não proíbe o acesso a um arquivo “locado” – apenas indica a existência do lock, e os processos precisam colaborar entre si para não sobrescrever o arquivo. Assim, para garantir a máxima compatibilidade multiplataforma, é preciso assumir que todos os locks são recomendados, e não obrigatórios;

·         Alguns SOs não permitem o lock de arquivos mapeados em memória;

·         SOs geralmente garantem locks a processos, e não a threads individuais. Dessa forma, locks adquiridos valem para a JVM como um todo, e não para cada thread. Ou seja, o mecanismo de file locking deve ser usado apenas para restringir o acesso a arquivos entre aplicações sendo executadas em JVMs diferentes; para threads na mesma JVM, o acesso ao arquivo deve ser controlado por blocos synchronized.

I/O não-bloqueante

Quando uma operação de I/O bloqueante é chamada, o controle de execução só é passado de volta à thread que a chamou quando a operação terminar ou falhar: a execução da thread fica bloqueada enquanto o método não retorna. Esse comportamento é muitas vezes indesejável, pois em vez de estar bloqueada esperando por uma operação de I/O, a thread poderia estar ativa executando outras tarefas.

Já quando a operação é não-bloqueante, o controle é imediatamente passado de volta à thread, e a operação de I/O é realizada em paralelo. Conseqüentemente, é necessário um mecanismo que sinalize o término da operação e indique o seu resultado. As operações de I/O não-bloqueante são oferecidas apenas por algumas classes derivadas de java.nio.channels.SelectableChannel. Na implementação atual do NIO (J2SE 1.4.2), apenas os channels relacionados a sockets (SocketChannel, ServerSocketChannel e DatagramChannel) oferecem operações de I/O não-bloqueante.

Tomemos como exemplo o método connect() da classe SocketChannel. Quando em modo bloqueante – o modo é definido através do método configureBlocking(boolean modo) – a chamada a esse método é bloqueada até que a conexão seja estabelecida ou um erro de I/O ocorra (por exemplo, timeout na conexão). Já em modo não ...

Quer ler esse conteúdo completo? Tenha acesso completo