A tabela a seguir resume as principais diferenças entre Java NIO e IO. Descreverei as diferenças em cada parte da tabela com mais detalhes.
Copie o código do código da seguinte forma:
IO NIO
Orientado a fluxo e orientado a buffer
IO bloqueador IO não bloqueador
Nenhum seletor
Orientado a fluxo e orientado a buffer
A primeira maior diferença entre Java NIO e IO é que IO é orientado a fluxo e NIO é orientado a buffer. Java IO é orientado a fluxo, o que significa que um ou mais bytes são lidos do fluxo por vez e, até que todos os bytes sejam lidos, eles não são armazenados em cache em nenhum lugar. Além disso, ele não pode mover dados no fluxo para frente ou para trás. Se você precisar mover os dados lidos do fluxo para frente e para trás, primeiro será necessário armazená-los em cache em um buffer. A abordagem orientada a buffer do Java NIO é um pouco diferente. Os dados são lidos em um buffer que é processado posteriormente, movendo-se para frente e para trás no buffer conforme necessário. Isso aumenta a flexibilidade no processamento. No entanto, você também precisa verificar se o buffer contém todos os dados que você precisa processar. Além disso, certifique-se de que, à medida que mais dados são lidos no buffer, os dados não processados no buffer não sejam substituídos.
IO bloqueador e não bloqueador
Vários fluxos de Java IO estão bloqueando. Isso significa que quando um thread chama read() ou write(), o thread é bloqueado até que alguns dados sejam lidos ou que os dados sejam completamente gravados. O thread não pode fazer mais nada durante este período. O modo sem bloqueio do Java NIO permite que um thread envie uma solicitação para ler dados de um determinado canal, mas só pode obter os dados atualmente disponíveis. Se nenhum dado estiver disponível no momento, nada será obtido. Em vez de manter o thread bloqueado, o thread pode continuar a fazer outras coisas até que os dados se tornem legíveis. O mesmo vale para gravações sem bloqueio. Um thread solicita a gravação de alguns dados em um canal, mas não precisa esperar que eles sejam completamente gravados. Enquanto isso, o thread pode fazer outras coisas. Threads normalmente usam tempo ocioso em IO sem bloqueio para executar operações de IO em outros canais, portanto, um único thread agora pode gerenciar vários canais de entrada e saída.
Seletores
Os seletores do Java NIO permitem que um único thread monitore vários canais de entrada. Você pode registrar vários canais usando um seletor e, em seguida, usar um thread separado para "selecionar" canais: esses canais já possuem entrada que pode ser processada. pronto para escrever. Esse mecanismo de seleção facilita que um único thread gerencie vários canais.
Como NIO e IO impactam o design de aplicativos
Quer você escolha uma caixa de ferramentas IO ou NIO, há vários aspectos que podem afetar o design do seu aplicativo:
1. Chamadas de API para classes NIO ou IO.
2. Processamento de dados.
3. O número de threads usados para processar dados.
Chamada de API
É claro que as chamadas de API ao usar o NIO parecem diferentes das do IO, mas isso não é inesperado porque, em vez de apenas ler um InputStream byte por byte, os dados devem primeiro ser lidos em um buffer e depois processados.
Processamento de dados
Usando o design NIO puro em comparação com o design IO, o processamento de dados também é afetado.
No design de IO, lemos dados byte por byte de InputStream ou Reader. Suponha que você esteja processando um fluxo de dados de texto baseado em linha, por exemplo:
Copie o código do código da seguinte forma:
Nome: Ana
Idade: 25
E-mail: [email protected]
Telefone: 1234567890
O fluxo de linhas de texto pode ser tratado assim:
Copie o código do código da seguinte forma:
Leitor BufferedReader = novo BufferedReader(new InputStreamReader(entrada));
String nomeLinha = leitor.readLine();
String idadeLine = leitor.readLine();
String emailLine = leitor.readLine();
String phoneLine = leitor.readLine();
Observe que o status do processamento é determinado pelo tempo de execução do programa. Em outras palavras, uma vez que o método reader.readLine() retornar, você terá certeza de que a linha de texto foi lida. É por isso que readline() bloqueia até que toda a linha tenha sido lida. Você também sabe que esta linha contém nomes; da mesma forma, quando a segunda chamada readline() retorna, você sabe que esta linha contém idades, etc. Como você pode ver, esse manipulador só é executado quando novos dados são lidos e sabe quais são os dados em cada etapa. Depois que um thread em execução tiver processado alguns dos dados lidos, ele não reverterá os dados (na maior parte). A figura a seguir também ilustra esse princípio:
(Java IO: Lendo dados de um fluxo de bloqueio) Embora uma implementação NIO seja diferente, aqui está um exemplo simples:
Copie o código da seguinte forma: ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
Observe a segunda linha, lendo bytes do canal em um ByteBuffer. Quando essa chamada de método retorna, você não sabe se todos os dados necessários estão no buffer. Tudo o que você sabe é que o buffer contém alguns bytes, o que dificulta um pouco o processamento.
Suponha que após a primeira chamada read(buffer), os dados lidos no buffer sejam apenas meia linha, por exemplo, "Nome: An", você pode processar os dados? Obviamente não, você precisa esperar até que toda a linha de dados seja lida no cache. Antes disso, qualquer processamento dos dados não terá sentido.
Então, como saber se o buffer contém dados suficientes para processar? Bem, você não sabe. Os métodos descobertos só podem visualizar dados no buffer. O resultado é que você terá que verificar os dados do buffer diversas vezes antes de saber que todos os dados estão no buffer. Isso não é apenas ineficiente, mas também pode sobrecarregar a solução de programação. Por exemplo:
Copie o código do código da seguinte forma:
Buffer ByteBuffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
while(!bufferFull(bytesRead) ) {
bytesRead = inChannel.read(buffer);
}
O método bufferFull() deve monitorar quantos dados foram lidos no buffer e retornar verdadeiro ou falso, dependendo se o buffer está cheio. Em outras palavras, se o buffer estiver pronto para ser processado, ele estará cheio.
O método bufferFull() verifica o buffer, mas deve permanecer no mesmo estado de antes do método bufferFull() ser chamado. Caso contrário, os próximos dados lidos no buffer poderão não ser lidos no local correto. Isso é impossível, mas é mais uma questão a ter em conta.
Se o buffer estiver cheio, ele poderá ser processado. Se não funcionar e fizer sentido no seu caso real, você poderá lidar com parte disso. Mas em muitos casos este não é o caso. A figura a seguir mostra "ciclo de dados do buffer pronto":
3) Número de threads usados para processar dados
O NIO permite gerenciar vários canais (conexões de rede ou arquivos) usando apenas um único thread (ou alguns), mas a desvantagem é que a análise dos dados pode ser mais complexa do que lê-los de um fluxo de bloqueio.
Se você precisar gerenciar milhares de conexões abertas simultaneamente que enviam apenas pequenas quantidades de dados por vez, como um servidor de chat, um servidor que implemente NIO pode ser uma vantagem. Da mesma forma, se você precisar manter muitas conexões abertas com outros computadores, como em uma rede P2P, pode ser uma vantagem usar um thread separado para gerenciar todas as suas conexões de saída. O plano de design para múltiplas conexões em um thread é o seguinte:
Java NIO: thread único gerenciando múltiplas conexões
Se você tiver um pequeno número de conexões usando largura de banda muito alta, enviando grandes quantidades de dados de uma só vez, talvez uma implementação típica de servidor IO possa ser uma boa opção. A figura a seguir ilustra um design típico de servidor IO:
Java IO: um design típico de servidor IO - uma conexão é tratada por um thread