1. Introdução
Existem muitos artigos sobre as diferenças e princípios de Java BIO, NIO e AIO, mas eles são discutidos principalmente entre BIO e NIO, enquanto há muito poucos artigos sobre AIO, e muitos deles são apenas introduções. os conceitos e exemplos de código.
Ao aprender sobre AIO, os seguintes fenômenos foram notados:
1. Java 7 foi lançado em 2011, que adicionou um modelo de programação chamado AIO chamado IO assíncrono, mas quase 12 anos se passaram e o middleware de estrutura de desenvolvimento usual ainda é dominado por NIO, como a estrutura de rede Netty, Mina, contêiner da Web Tomcat, Ressaca.
2. Java AIO também é chamado de NIO 2.0. Também é baseado em NIO?
3. Netty abandonou o suporte da AIO. https://github.com/netty/netty/issues/2515
4. AIO parece ter apenas resolvido o problema e liberado uma solidão.
Esses fenômenos irão inevitavelmente confundir muitas pessoas, então quando decidi escrever este artigo, não queria simplesmente repetir o conceito de AIO, mas como analisar, pensar e entender a essência do Java AIO através do fenômeno.
2. O que é assíncrono
2.1 Assincronia como a conhecemos
O A de AIO significa Assíncrono.Antes de entender o princípio de AIO, vamos esclarecer que tipo de conceito é "assíncrono".
Falando em programação assíncrona, ela ainda é relativamente comum no desenvolvimento normal, como os exemplos de código a seguir:
@Async
public void create() {
//TODO
}
public void build() {
executor.execute(() -> build());
}
Seja anotado com @Async ou enviando tarefas para o pool de threads, todos terminam com o mesmo resultado, que é entregar a tarefa a ser executada para outro thread para execução.
Neste momento, pode-se considerar aproximadamente que o chamado "assíncrono" é multiencadeado e executa tarefas.
2.2 Java BIO e NIO são síncronos ou assíncronos?
Se Java BIO e NIO são síncronos ou assíncronos, primeiro fazemos programação assíncrona de acordo com a ideia de assincronia.
2.2.1 Exemplo BIO
byte [] data = new byte[1024];
InputStream in = socket.getInputStream();
in.read(data);
// 接收到数据,异步处理
executor.execute(() -> handle(data));
public void handle(byte [] data) {
// TODO
}
Quando BIO read(), embora o thread esteja bloqueado, ao receber dados, um thread pode ser iniciado de forma assíncrona para processar.
2.2.2 Exemplo NIO
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
executor.execute(() -> {
try {
channel.read(byteBuffer);
handle(byteBuffer);
} catch (Exception e) {
}
});
}
}
public static void handle(ByteBuffer buffer) {
// TODO
}
Da mesma forma, embora NIO read() não bloqueie, ele pode bloquear a espera de dados através de select().Quando há dados para ler, ele inicia uma thread assincronamente para ler e processar dados.
2.2.3 Desvios de Entendimento
Neste momento, juramos que se o BIO e o NIO do Java são assíncronos ou síncronos, depende do seu humor. Se você estiver feliz em dar a ele um multi-thread, é assíncrono.
Mas se for esse o caso, depois de ler muitos artigos de blog, fica basicamente esclarecido que BIO e NIO são sincronizados.
Então, onde está o problema?O que causou o desvio em nossa compreensão?
Esse é o problema do quadro de referência. Ao estudar física antes, se os passageiros do ônibus estão em movimento ou parados, é necessário um quadro de referência. Se o solo for usado como referência, ele está em movimento e o ônibus é usado como uma referência, ele é estacionário.
O mesmo vale para o Java IO. É necessário um sistema de referência para definir se ele é síncrono ou assíncrono. Como estamos discutindo qual é o modo de IO, é necessário entender as operações de leitura e gravação do IO, enquanto outros iniciam outro. Os encadeamentos para processar dados já estão fora do escopo da leitura e gravação de IO e não devem ser envolvidos.
2.2.4 Tentando definir assíncrono
Portanto, tomando como referência o evento das operações de leitura e gravação de IO, primeiro tentamos definir a thread que inicia a leitura e gravação de IO (a thread que chama read e write) e a thread que realmente opera a leitura e gravação de IO. eles são o mesmo thread, então chame-o de síncrono, caso contrário, assíncrono .
-
Obviamente, BIO só pode ser síncrono. Chamar in.read() bloqueia o thread atual. Quando os dados são retornados, o thread original recebe os dados.
-
E NIO também é chamado de sincronização, e o motivo é o mesmo: ao chamar channel.read(), embora o thread não bloqueie, ainda é o thread atual que lê os dados.
De acordo com essa ideia, o AIO deve ser o thread que inicia a leitura e a gravação do IO, e o thread que realmente recebe os dados pode não ser o mesmo thread. É este o
caso? Vamos iniciar o código Java AIO agora.
2.3 Exemplo de programa Java AIO
2.3.1 Programa do servidor AIO
public class AioServer {
public static void main(String[] args) throws IOException {
System.out.println(Thread.currentThread().getName() + " AioServer start");
AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open()
.bind(new InetSocketAddress("127.0.0.1", 8080));
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {
System.out.println(Thread.currentThread().getName() + " client is connected");
ByteBuffer buffer = ByteBuffer.allocate(1024);
clientChannel.read(buffer, buffer, new ClientHandler());
}
@Override
public void failed(Throwable exc, Void attachment) {
System.out.println("accept fail");
}
});
System.in.read();
}
}
public class ClientHandler implements CompletionHandler<Integer, ByteBuffer> {
@Override
public void completed(Integer result, ByteBuffer buffer) {
buffer.flip();
byte [] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println(Thread.currentThread().getName() + " received:" + new String(data, StandardCharsets.UTF_8));
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
}
}
2.3.2 Programa cliente AIO
public class AioClient {
public static void main(String[] args) throws Exception {
AsynchronousSocketChannel channel = AsynchronousSocketChannel.open();
channel.connect(new InetSocketAddress("127.0.0.1", 8080));
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Java AIO".getBytes(StandardCharsets.UTF_8));
buffer.flip();
Thread.sleep(1000L);
channel.write(buffer);
}
}
2.3.3 Conclusão da conjectura de definição assíncrona
Execute os programas de servidor e cliente separadamente
No resultado da execução do servidor,
O thread principal inicia uma chamada para serverChannel.accept e adiciona um CompletionHandler para monitorar o retorno de chamada.Quando um cliente se conecta, o thread Thread-5 executa o método de retorno de chamada concluído de accept.
Imediatamente depois, Thread-5 iniciou a chamada clientChannel.read e adicionou um CompletionHandler para monitorar o retorno de chamada.Ao receber dados, Thread-1 executou o método de retorno de chamada concluído de leitura.
Esta conclusão é consistente com a conjectura assíncrona acima. A thread que inicia a operação IO (como aceitar, ler, escrever) não é a mesma que a thread que finalmente conclui a operação. Chamamos esse modo IO de AIO .
É claro que definir AIO dessa forma é apenas para nosso entendimento.Na prática, a definição de IO assíncrona pode ser mais abstrata.
3. O exemplo de AIO leva a perguntas de reflexão
1. Quem criou o thread executando o método Completed() e quando ele foi criado?
2. Como implementar o monitoramento de eventos de registro AIO e callback de execução?
3. Qual é a essência do monitoramento de retorno de chamada?
3.1 Questão 1: Quem criou a thread que executa o método complete(), e quando ela foi criada
Geralmente tal problema precisa ser entendido desde a entrada do programa, mas está relacionado com a thread, de fato, é possível localizar como a thread roda a partir do estado running da pilha de threads.
Execute apenas o programa do servidor AIO, o cliente não executa, imprima a pilha de threads (Nota: o programa é executado na plataforma Linux e outras plataformas são ligeiramente diferentes)
Analise a pilha de threads e descubra que o programa inicia tantos threads
1. Thread Thread-0 está bloqueado no método EPoll.wait()
2. Linha Linha-1, Linha-2. . . Thread-n (n é o mesmo que o número de núcleos da CPU) pega () tarefas da fila de bloqueio e bloqueia aguardando o retorno de uma tarefa.
Neste ponto, a próxima conclusão pode ser tirada provisoriamente:
Depois que o programa do servidor AIO é iniciado, esses encadeamentos são criados e todos os encadeamentos ficam em um estado de espera bloqueado.
Além disso, descobri que a execução desses threads está relacionada ao Epoll. Quando se trata do Epoll, temos a impressão de que o Java NIO é implementado com o Epoll na parte inferior da plataforma Linux. O Java AIO também é implementado com o Epoll ? Para confirmar esta conclusão, discutimos a partir da próxima pergunta
3.2 Pergunta 2: Como implementar o monitoramento de eventos de registro AIO e callback de execução
Com esse problema em mente, quando li e analisei o código-fonte, descobri que o código-fonte é muito longo e a análise do código-fonte é um processo chato, que pode facilmente afastar os leitores.
Para a compreensão de processos longos e códigos logicamente complexos, podemos entender seus vários contextos e descobrir quais processos centrais.
Pegue o listener de registro lido como um exemplo clientChannel.read(…), seu principal processo principal é:
1. Registrar evento -> 2. Ouvir evento -> 3. Processar evento
3.2.1 1. Evento de inscrição
O evento de registro chama a função EPoll.ctl(…) e o último parâmetro desta função é usado para especificar se é único ou permanente. Os eventos de código acima | EPOLLONSHOT significam literalmente que é único.
3.2.2 2. Monitorar eventos
3.2.3 3. Tratamento de eventos
3.2.4 Resumo dos principais processos
Depois de analisar o fluxo de código acima, você descobrirá que os três eventos que devem ser experimentados para cada leitura e gravação de IO são únicos, ou seja, depois que o evento é processado, esse processo termina. Se você quiser continuar no próximo IO Para ler e escrever, é preciso começar tudo de novo. Dessa forma, haverá o chamado retorno de chamada da morte (o próximo método de retorno de chamada é adicionado ao método de retorno de chamada), o que aumenta muito a complexidade da programação.
3.3 Pergunta 3: Qual é a essência do monitoramento de callbacks?
Deixe-me falar sobre a conclusão primeiro. A essência do chamado retorno de chamada de monitoramento é o thread do modo de usuário, que chama a função do modo kernel (falando com precisão, API, como read, write, epollWait). não retornado, o thread do usuário está bloqueado. Quando a função retorna, o thread bloqueado é ativado e a chamada função de retorno de chamada é executada .
Para entender essa conclusão, devemos primeiro introduzir vários conceitos
3.3.1 Chamadas de sistema e chamadas de função
chamada de função:
Encontre uma função e execute comandos relacionados na função
Chamada do sistema:
O sistema operacional fornece uma interface de programação para os aplicativos do usuário, a chamada API.
Processo de execução da chamada do sistema:
1. Passe os parâmetros de chamada do sistema
2. Execute instruções interceptadas, alterne do modo de usuário para o modo principal, porque as chamadas do sistema geralmente precisam ser executadas no modo principal
3. Execute o programa de chamada do sistema
4. Retorne ao estado do usuário
3.3.2 Comunicação entre o modo usuário e o modo kernel
Modo de usuário -> Modo de kernel, apenas por meio de chamadas do sistema.
Modo kernel -> modo usuário, o modo kernel não sabe quais funções o programa do modo usuário possui, quais são os parâmetros e onde está o endereço. Portanto, é impossível para o kernel chamar funções no modo usuário, mas apenas enviando sinais.Por exemplo, o comando kill para fechar o programa é permitir que o programa do usuário saia normalmente enviando sinais.
Como é impossível para o estado do kernel chamar funções ativamente no estado do usuário, por que há um retorno de chamada?Pode-se dizer apenas que esse chamado retorno de chamada é na verdade um estado de usuário autodirigido e executado. Ele não apenas monitora, mas também executa a função de retorno de chamada.
3.3.3 Verifique a conclusão com exemplos práticos
Para verificar se essa conclusão é convincente, por exemplo, o IntelliJ IDEA, que geralmente é usado para desenvolver e escrever código, escuta eventos de mouse e teclado e manipula eventos.
De acordo com a convenção, primeiro imprima a pilha de encadeamentos e você descobrirá que o encadeamento "AWT-XAWT" é responsável por monitorar eventos como mouse e teclado, e o encadeamento "AWT-EventQueue" é responsável pelo processamento de eventos.
Localizando o código específico, você pode ver que "AWT-XAWT" está fazendo um loop while, chamando a função waitForEvents para aguardar o retorno do evento. Se não houver evento, o thread foi bloqueado lá.
4. Qual é a essência do Java AIO?
1. Como o modo kernel não pode chamar diretamente as funções do modo usuário, a essência do Java AIO é implementar a assincronia apenas no modo usuário. Não alcança a assincronia no sentido ideal.
assíncrono ideal
O que é assincronia no sentido ideal? Aqui está um exemplo de compras online
Duas funções, consumidor A e entregador B
-
Quando A estiver fazendo compras online, preencha o endereço residencial para pagar e enviar o pedido, o que equivale a registrar o evento de monitoramento
-
O comerciante entrega as mercadorias e B entrega o item na porta de A, o que equivale a um retorno de chamada.
Depois que A faz o pedido online, ele não precisa se preocupar com o processo de entrega subsequente e pode continuar fazendo outras coisas. B não se importa se A está em casa ou não ao entregar as mercadorias. De qualquer forma, basta jogar as mercadorias na porta da casa. As duas pessoas não dependem uma da outra e não interferem uma na outra .
Supondo que as compras de A sejam feitas no modo de usuário e a entrega expressa de B seja feita no modo kernel, esse tipo de modo de operação do programa é ideal demais e não pode ser realizado na prática.
Assincronia na realidade
A mora em uma área residencial de alto padrão e não pode entrar à vontade, e o entregador só pode ser entregue no portão da área residencial.
A comprou um produto relativamente pesado, como uma TV, porque A estava indo trabalhar e não estava em casa, então pediu a um amigo C que ajudasse a levar a TV para sua casa.
Antes de sair para o trabalho, A cumprimenta o segurança D na porta, dizendo que hoje será entregue uma TV, quando for entregue na porta da comunidade, por favor, ligue para C e peça para ele vir buscá-la.
-
Nesse ponto, A faz um pedido e cumprimenta D, o que equivale a registrar um evento. No AIO é o evento de registro EPoll.ctl(...).
-
O segurança agachado na porta equivale a ouvir o evento. Em AIO, é a thread Thread-0. Faça EPoll.wait(…)
-
O mensageiro entregou a TV na porta, o que equivale à chegada de um evento IO.
-
O segurança avisa a C que a TV chegou, e C vem mover a TV, o que equivale a lidar com o incidente.
No AIO, Thread-0 envia tarefas para a fila de tarefas.
Thread-1 ~n para buscar dados e executar o método de retorno de chamada.
Durante todo o processo, o segurança D teve que ficar agachado o tempo todo, não podendo sair nem um centímetro, caso contrário a TV seria roubada quando fosse entregue na porta.
O amigo C também tem que ficar na casa de A. Ele é confiado por alguém, mas a pessoa não está lá quando as coisas chegam, isso é um pouco desonesto.
Portanto, a assincronia real e a assincronia ideal são independentes uma da outra e não interferem uma na outra, esses dois pontos são contrários um ao outro . O papel de segurança é o maior, e este é o momento de destaque de sua vida.
Registrando eventos, ouvindo eventos, processando eventos e habilitando multi-threading no processo assíncrono, os iniciadores desses processos são todos manipulados pelo modo de usuário, portanto, o Java AIO implementa apenas a assincronia no modo de usuário, que é bloqueado primeiro com o BIO e NIO , a essência de iniciar o processamento de encadeamento assíncrono após o bloqueio da ativação é a mesma.
2. Java AIO é o mesmo que NIO, e os métodos de implementação subjacentes de cada plataforma também são diferentes. EPoll é usado no Linux, IOCP é usado no Windows e KQueue é usado no Mac OS. O princípio é o mesmo, todos requerem um thread de usuário para bloquear e aguardar eventos de IO e um pool de threads para processar eventos da fila.
3. A razão pela qual Netty removeu o AIO é que o AIO não é superior ao NIO em termos de desempenho. Embora o Linux também tenha um conjunto de implementações AIO nativas (semelhantes ao IOCP no Windows), o Java AIO não é usado no Linux, mas é implementado com EPoll.
4. Java AIO não suporta UDP
5. O método de programação AIO é um pouco complicado, como "death callback"