Problemas no java.io

Uma das principais preocupações dos desenvolvedores de aplicações que precisam fazer muito IO (também chamadas de IO Bound) é o desempenho...

Problemas no java.io

 

Uma das principais preocupações dos desenvolvedores de aplicações que precisam fazer muito IO (também chamadas de IO Bound) é o desempenho. Os desenvolvedores passam horas projetando as aplicações para otimizar a realização de IO e depois, quase nunca satisfeitos, ainda dedicam outras tantas fazendo ajustes. Utilizando java.io, os desenvolvedores Java contam com uma API limitada, com abstrações distantes das oferecidas pelo sistema operacional e que tem muitos problemas em sua implementação. Como resultado os programadores precisavam fazer malabarismos no projeto e na implementação das aplicações para alcançar um bom desempenho.

 

Abstrações

Um dos carros chefes da API java.io são os IO Streams. Oferencendo abstrações de alto nível baseadas em composição, eles facilitam a realização de tarefas simples. Em contrapartida, dificultam a realização de tarefas mais complexas, pois o programador precisa ter um conhecimento detalhado de toda a API para saber quais objetos e em que ordem estes devem ser compostos. Para ilustrar, citamos um exemplo bem simples: ler um arquivo que contém dados do tipo long. Não esquecendo do desempenho, uma das formas de solucionar este problema seria:

 

  ...

  DataInputStream inputStream = new DataInputStream( new BufferedInputStream( new FileInputStream( fileName )));

  long aLong = inputStream.readLong();

  ...

 

O problema com essa implementação é que o ganho de desempenho na utilização do BufferedInputStream é perdido (em parte), pois o DataInputStream lê apenas um byte por vez. Logo, para ler cada long do arquivo precisaríamos fazer oito chamadas ao método read do BufferedInputStream.

 

Distante das abstrações oferecidas pelo sistema operacional, as abstrações oferecidas pelos IO Streams limitam o poder do desenvolvedor, não permitindo, por exemplo, o uso de Direct Memory Access (DMA). Esta característica (essencial nos computadores modernos) permite que o sistema operacional transfira dados entre dispositivos e a memória sem utilização do barramento da CPU, conseguindo assim diminuir bastante o tempo de execução de certas operações.

 

Sincronização

Criada para permitir a construção segura e fácil de sistemas computacionais distribuídos, a linguagem Java apresenta uma API repleta de métodos públicos sincronizados e o pacote java.io não é diferente. A maioria dos IO Streams possuem seus métodos públicos sincronizados, dentre eles os métodos para leitura e escrita. Esta decisão de implementação não permite ao programador evitar a aquisição destes locks, que podem muitas vezes ser desnecessários.

 

Ainda nesse contexto, outro caso que merece destaque é o dos StringBuffers. Todos os métodos públicos (com exceção dos construtores) são sincronizados. Sendo esta classe usada para implementação do operador de concatenação de Strings em Java, para executar:

 

  String hello = “Hello ” + “ New IO ” + “ users”;

 

A JVM executa:

 

  String hello = new StringBuffer.append(“Hello ”).append(“ New IO ”).append(“ users ”).toString();

 

O que implica na aquisição de quatro locks. É importante notar que essa aquisição pode ser completamente desnecessária caso essa linha seja executada por apenas uma Thread.

 

IO sempre bloqueante

Supondo que temos a seguinte situação: Uma aplicação que suporte várias requisições simultâneas (Um servidor Web, por exemplo). A forma mais conhecida de se fazer isso é criando uma Thread para atender cada requisição.

 

Essa forma de solucionar o problema só existe por que a API oferecia apenas a opção de realizar IO de forma bloqueante (ou síncrona). Isso deixou de fazer sentido quando os sistemas operacionais começaram a suportar operações de IO de forma não bloqueante (ou assíncrona). Em poucas palavras, uma operação não bloqueante ocorre da seguinte forma: Uma Thread inicia a operação de IO e devido a natureza não bloqueante da operação ela seria imediatamente liberada. A partir daí ela poderia ficar checando se a operação acabou ou então se registar como interessada no final da operação.

 

Note que essa funcionalidade torna a realização de IO muito mais eficiente, dado que o servidor não precisa arcar com o custo de criação e destruição de Threads de aplicação. O desenvolvedor também deixa de se preocupar com detalhes típicos do modo síncrono, entre eles, que nenhuma Thread execute por um intervalo muito grande de tempo, o que poderia levar a um estouro no número de Threads criadas pela JVM. 

 

Em http://www.ddj.com/dept/java/184406242 podemos ver como um servidor pode ter seu desempenho melhorado com a utilização do modo não bloqueante. Na Figura 1, expomos na primeira coluna o tempo (em minutos) e nas colunas seguintes o número de transações processadas utilizando as duas formas de fazer IO.

 

 

Figura 1: Avaliação de desempenho entre as duas formas de fazer IO

 

Conclusões

Nesse artigo mostramos os problemas que existem com a API java.io. Com isso queremos motivar o leitor a pensar sobre um problema muito comum para muitos desenvolvedores Java: o bom desempenho das aplicações que realizam muitas operações de IO. Este, pode ser impossível de ser obtido se não mudar-mos a forma que pensamos e implementamos nossas aplicações. Um bom ferramental pode ser encontrado (a partir da JDK 1.4) na API New IO. É sobre essa API, tão rica em novidades, que tratarei nos próximos artigos.

 

Referências

lhttp://java.sun.com/j2se/1.5.0/docs/api/java/io/InputStream.html

lhttp://java.sun.com/j2se/1.4.2/docs/api/java/io/DataInputStream.html

lhttp://java.sun.com/j2se/1.4.2/docs/api/java/io/RandomAccessFile.html

lhttp://en.wikipedia.org/wiki/Direct_memory_access

lhttp://en.wikipedia.org/wiki/Non-blocking_IO

lhttp://www.javapractices.com/Topic4.cjp

lhttp://www.ddj.com/dept/java/184406242

lhttp://www.conexaojava.com.br/conexaojava04/download/palestras/Java.nio-a.nova.entrada.e.saida.do.Java-Conexao.Java.2004.pdf#search=%22conex%C3%A3o%20java%20nio%22

Artigos relacionados