1. Introduction
Il existe de nombreux articles de ce type sur les différences et les principes de Java BIO, NIO et AIO, mais ils sont principalement discutés entre BIO et NIO, alors qu'il existe très peu d'articles sur AIO, et beaucoup d'entre eux ne sont que des introductions. les concepts et les exemples de code.
Lors de l'apprentissage de l'AIO, les phénomènes suivants ont été remarqués :
1. Java 7 est sorti en 2011, qui a ajouté un modèle de programmation appelé AIO appelé IO asynchrone, mais près de 12 ans se sont écoulés, et le middleware de développement habituel est toujours dominé par NIO, comme le framework réseau Netty, Mina, conteneur Web Tomcat, ressac.
2. Java AIO est aussi appelé NIO 2.0, est-il également basé sur NIO ?
3. Netty a abandonné le support d'AIO. https://github.com/netty/netty/issues/2515
4. AIO semble avoir seulement résolu le problème et libéré une solitude.
Ces phénomènes vont inévitablement dérouter beaucoup de gens, alors quand j'ai décidé d'écrire cet article, je ne voulais pas simplement répéter le concept d'AIO, mais comment analyser, penser et comprendre l'essence de Java AIO à travers le phénomène.
2. Qu'est-ce que l'asynchrone
2.1 L'asynchronisme tel que nous le connaissons
Le A d'AIO signifie Asynchrone.Avant de comprendre le principe d'AIO, clarifions ce qu'est le concept "asynchrone".
En parlant de programmation asynchrone, elle est encore relativement courante dans le développement normal, comme les exemples de code suivants :
@Async
public void create() {
//TODO
}
public void build() {
executor.execute(() -> build());
}
Qu'il soit annoté avec @Async ou qu'il soumette des tâches au pool de threads, ils aboutissent tous au même résultat, qui consiste à transmettre la tâche à exécuter à un autre thread pour exécution.
A ce stade, on peut grosso modo considérer que le soi-disant "asynchrone" est multi-thread et exécute des tâches.
2.2 Java BIO et NIO sont-ils synchrones ou asynchrones ?
Que Java BIO et NIO soient synchrones ou asynchrones, on fait d'abord de la programmation asynchrone selon l'idée d'asynchronie.
2.2.1 Exemple BIO
byte [] data = new byte[1024];
InputStream in = socket.getInputStream();
in.read(data);
// 接收到数据,异步处理
executor.execute(() -> handle(data));
public void handle(byte [] data) {
// TODO
}
Lorsque BIO read(), bien que le thread soit bloqué, lors de la réception de données, un thread peut être démarré de manière asynchrone pour être traité.
2.2.2 Exemple 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
}
De la même manière, bien que NIO read() soit non bloquant, il peut bloquer l'attente de données via select(). Lorsqu'il y a des données à lire, il démarre un thread de manière asynchrone pour lire et traiter les données.
2.2.3 Écarts de compréhension
À l'heure actuelle, nous jurons que le fait que BIO et NIO de Java soient asynchrones ou synchrones dépend de votre humeur.Si vous êtes heureux de lui donner un multi-thread, il est asynchrone.
Mais si tel est le cas, après avoir lu de nombreux articles de blog, il est essentiellement précisé que BIO et NIO sont synchronisés.
Alors, où est le problème ? Qu'est-ce qui a causé l'écart dans notre compréhension ?
C'est le problème du cadre de référence. Lorsque l'on étudie la physique auparavant, savoir si les passagers du bus sont en mouvement ou à l'arrêt nécessite un cadre de référence. Si le sol est utilisé comme référence, il se déplace et le bus est utilisé comme une référence, il est immobile.
Il en va de même pour Java IO. Un système de référence est nécessaire pour définir s'il est synchrone ou asynchrone. Puisque nous discutons de quel mode d'IO il s'agit, il est nécessaire de comprendre les opérations de lecture et d'écriture d'IO, tandis que d'autres en lancent une autre. Les threads pour traiter les données sont déjà hors de portée de la lecture et de l'écriture d'E/S, et ils ne devraient pas être impliqués.
2.2.4 Essayer de définir asynchrone
Par conséquent, en prenant l'événement des opérations de lecture et d'écriture d'E/S comme référence, nous essayons d'abord de définir le thread qui initie la lecture et l'écriture d'E/S (le thread qui appelle la lecture et l'écriture) et le thread qui opère réellement la lecture et l'écriture d'E/S. ils sont le même thread, alors Appelez-le synchronous, sinon asynchronous .
-
Évidemment, BIO ne peut être que synchrone. L'appel de in.read() bloque le thread en cours. Lorsque les données sont renvoyées, le thread d'origine reçoit les données.
-
Et NIO est aussi appelé synchronisation, et la raison est la même : lors de l'appel de channel.read(), bien que le thread ne bloque pas, c'est toujours le thread en cours qui lit les données.
Selon cette idée, AIO devrait être le thread qui initie la lecture et l'écriture IO, et le thread qui reçoit réellement les données peut ne pas être le même thread. Est-ce le
cas ? Commençons maintenant le code Java AIO.
2.3 Exemple de programme Java AIO
2.3.1 Programme serveur 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 Programme client 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 Conclusion de la conjecture de définition asynchrone
Exécutez les programmes serveur et client séparément
Suite à l'exécution du serveur,
Le thread principal initie un appel à serverChannel.accept et ajoute un CompletionHandler pour surveiller le rappel. Lorsqu'un client se connecte, le thread Thread-5 exécute la méthode de rappel terminée de accept.
Immédiatement après, Thread-5 a lancé l'appel clientChannel.read et a ajouté un CompletionHandler pour surveiller le rappel. Lors de la réception de données, Thread-1 a exécuté la méthode de rappel terminée de read.
Cette conclusion est cohérente avec la conjecture asynchrone ci-dessus. Le thread qui lance l'opération IO (comme accepter, lire, écrire) n'est pas le même que le thread qui termine finalement l'opération. Nous appelons ce mode IO AIO .
Bien sûr, définir AIO de cette manière est juste pour notre compréhension.En pratique, la définition d'IO asynchrone peut être plus abstraite.
3. L'exemple AIO suscite des questions de réflexion
1. Qui a créé le thread exécutant la méthode complete() et quand a-t-il été créé ?
2. Comment implémenter la surveillance des événements d'enregistrement AIO et le rappel d'exécution ?
3. Quelle est l'essence du rappel de surveillance ?
3.1 Question 1 : Qui a créé le thread qui exécute la méthode filled(), et quand a-t-il été créé
Généralement, un tel problème doit être compris dès l'entrée du programme, mais il est lié au thread. En fait, il est possible de localiser le fonctionnement du thread à partir de l'état d'exécution de la pile de threads.
N'exécutez que le programme serveur AIO, le client ne s'exécute pas, imprimez la pile de threads (Remarque : le programme s'exécute sur la plate-forme Linux et les autres plates-formes sont légèrement différentes)
Analysez la pile de threads et constatez que le programme démarre autant de threads
1. Thread Thread-0 est bloqué sur la méthode EPoll.wait()
2. Enfilez Thread-1, Thread-2. . . Thread-n (n est le même que le nombre de cœurs de processeur) prend () les tâches de la file d'attente de blocage et bloque en attendant le retour d'une tâche.
À ce stade, la conclusion suivante peut être provisoirement tirée :
Après le démarrage du programme serveur AIO, ces threads sont créés et les threads sont tous dans un état d'attente bloqué.
De plus, j'ai trouvé que le fonctionnement de ces threads est lié à Epoll.En ce qui concerne Epoll, on a l'impression que Java NIO est implémenté avec Epoll en bas de la plate-forme Linux.Java AIO est-il également implémenté avec Epoll ? Afin de confirmer cette conclusion, nous discutons à partir de la question suivante
3.2 Question 2 : Comment implémenter la surveillance des événements d'enregistrement AIO et le rappel d'exécution
Avec ce problème à l'esprit, lorsque j'ai lu et analysé le code source, j'ai constaté que le code source est très long et que l'analyse du code source est un processus ennuyeux, qui peut facilement éloigner les lecteurs.
Pour la compréhension d'un processus long et d'un code logiquement complexe, nous pouvons saisir ses différents contextes et découvrir quels processus de base.
Prenons l'écouteur d'enregistrement lu comme exemple clientChannel.read(…), son principal processus de base est :
1. Enregistrer l'événement -> 2. Écouter l'événement -> 3. Traiter l'événement
3.2.1 1. Événement d'inscription
L'événement d'enregistrement appelle la fonction EPoll.ctl(…), et le dernier paramètre de cette fonction est utilisé pour spécifier s'il est ponctuel ou permanent. Les événements de code ci-dessus | EPOLLONSHOT signifie littéralement qu'il s'agit d'un événement unique.
3.2.2 2. Surveiller les événements
3.2.3 3. Gestion des événements
3.2.4 Résumé des processus de base
Après avoir analysé le flux de code ci-dessus, vous constaterez que les trois événements qui doivent être expérimentés pour chaque lecture et écriture d'E/S sont ponctuels, c'est-à-dire qu'une fois l'événement traité, ce processus est terminé. IO Pour lire et écrire, il faut tout recommencer. De cette façon, il y aura un soi-disant rappel de mort (la méthode de rappel suivante est ajoutée à la méthode de rappel), ce qui augmente considérablement la complexité de la programmation.
3.3 Question 3 : Quelle est l'essence de la surveillance des rappels ?
Permettez-moi de parler d'abord de la conclusion. L'essence de ce que l'on appelle le rappel de surveillance est le thread en mode utilisateur, qui appelle la fonction en mode noyau (pour être précis, l'API, telle que read, write, epollWait). Lorsque la fonction a n'est pas renvoyé, le thread utilisateur est bloqué. Lorsque la fonction revient, le thread bloqué est réveillé et la fonction dite de rappel est exécutée .
Pour comprendre cette conclusion, nous devons d'abord introduire plusieurs concepts
3.3.1 Appels système et appels de fonction
appel de fonction :
Trouver une fonction et exécuter les commandes associées dans la fonction
Appel système :
Le système d'exploitation fournit une interface de programmation aux applications utilisateur, appelée API.
Processus d'exécution des appels système :
1. Passer les paramètres d'appel système
2. Exécutez les instructions piégées, passez du mode utilisateur au mode principal, car les appels système doivent généralement être exécutés en mode principal
3. Exécutez le programme d'appel système
4. Retour à l'état utilisateur
3.3.2 Communication entre le mode utilisateur et le mode noyau
Mode utilisateur -> mode noyau, uniquement via des appels système.
Mode noyau -> mode utilisateur, le mode noyau ne sait pas quelles sont les fonctions du programme en mode utilisateur, quels sont les paramètres et où se trouve l'adresse. Par conséquent, il est impossible pour le noyau d'appeler des fonctions en mode utilisateur, mais uniquement en envoyant des signaux. Par exemple, la commande kill pour fermer le programme consiste à laisser le programme utilisateur se terminer normalement en envoyant des signaux.
Puisqu'il est impossible pour l'état du noyau d'appeler activement des fonctions dans l'état utilisateur, pourquoi y a-t-il un rappel ? On peut seulement dire que ce soi-disant rappel est en fait un état utilisateur auto-dirigé et auto-exécuté. Il surveille non seulement, mais exécute également la fonction de rappel.
3.3.3 Vérifier la conclusion avec des exemples pratiques
Afin de vérifier si cette conclusion est convaincante, par exemple, IntelliJ IDEA, qui est généralement utilisé pour développer et écrire du code, écoute les événements de la souris et du clavier et gère les événements.
Selon la convention, imprimez d'abord la pile de threads, et vous constaterez que le thread "AWT-XAWT" est responsable de la surveillance des événements tels que la souris et le clavier, et que le thread "AWT-EventQueue" est responsable du traitement des événements.
En localisant le code spécifique, vous pouvez voir que "AWT-XAWT" effectue une boucle while, appelant la fonction waitForEvents pour attendre le retour de l'événement. S'il n'y a pas d'événement, le fil y a été bloqué.
4. Quelle est l'essence de Java AIO ?
1. Étant donné que le mode noyau ne peut pas appeler directement les fonctions du mode utilisateur, l'essence de Java AIO est d'implémenter l'asynchronisme uniquement en mode utilisateur. Il n'atteint pas l'asynchronisme au sens idéal.
idéal asynchrone
Qu'est-ce que l'asynchronisme au sens idéal ? Voici un exemple d'achat en ligne
Deux rôles, consommateur A et coursier B
-
Lorsque A fait des achats en ligne, remplissez l'adresse du domicile pour payer et soumettre la commande, ce qui équivaut à enregistrer l'événement de surveillance
-
Le commerçant livre la marchandise et B livre l'article à la porte de A, ce qui équivaut à un rappel.
Une fois que A a passé la commande en ligne, il n'a pas à se soucier du processus de livraison ultérieur et peut continuer à faire d'autres choses. B ne se soucie pas de savoir si A est à la maison ou non lors de la livraison de la marchandise. Quoi qu'il en soit, il suffit de jeter la marchandise à la porte de la maison. Les deux personnes ne dépendent pas l'une de l'autre et ne se gênent pas .
En supposant que les achats de A sont effectués en mode utilisateur et que la livraison express de B est effectuée en mode noyau, ce type de mode de fonctionnement du programme est trop idéal et ne peut pas être réalisé en pratique.
Asynchronisme dans la réalité
A vit dans un quartier résidentiel haut de gamme et ne peut pas entrer à volonté, et le coursier ne peut être livré qu'à la porte du quartier résidentiel.
A a acheté un produit relativement lourd, comme un téléviseur, parce que A se rendait au travail et n'était pas à la maison, alors il a demandé à un ami C de l'aider à déplacer le téléviseur chez lui.
Avant que A ne parte travailler, il salue l'agent de sécurité D à la porte en disant qu'un téléviseur sera livré aujourd'hui. Lorsqu'il sera livré à la porte de la communauté, veuillez appeler C et lui demander de venir le chercher.
-
À ce stade, A passe une commande et salue D, ce qui équivaut à enregistrer un événement. Dans AIO, il s'agit de l'événement d'enregistrement EPoll.ctl(...) .
-
L'agent de sécurité accroupi à la porte équivaut à écouter l'événement. En AIO, c'est le thread Thread-0. Do EPoll.wait(…)
-
Le coursier a livré le téléviseur à la porte, ce qui équivaut à l'arrivée d'un événement IO.
-
L'agent de sécurité prévient C que le téléviseur est arrivé, et C vient déplacer le téléviseur, ce qui équivaut à gérer l'incident.
Dans AIO, Thread-0 soumet des tâches à la file d'attente de tâches.
Thread-1 ~n pour récupérer les données et exécuter la méthode de rappel.
Pendant tout le processus, l'agent de sécurité D a dû s'accroupir tout le temps et ne pouvait même pas laisser un pouce, sinon le téléviseur serait volé lorsqu'il serait livré à la porte.
L'ami C doit aussi rester chez A. Il est confié par quelqu'un, mais la personne n'est pas là quand les choses arrivent, c'est un peu malhonnête.
Par conséquent, l'asynchronisme réel et l'asynchronisme idéal sont indépendants l'un de l'autre et n'interfèrent pas l'un avec l'autre.Ces deux points sont contraires l'un à l'autre . Le rôle de la sécurité est le plus grand, et c'est le moment fort de sa vie.
En enregistrant des événements, en écoutant des événements, en traitant des événements et en activant le multi-threading dans le processus asynchrone, les initiateurs de ces processus sont tous gérés par le mode utilisateur, donc Java AIO n'implémente l'asynchronisme qu'en mode utilisateur, qui est d'abord bloqué avec BIO et NIO , l'essence du démarrage du traitement des threads asynchrones après le blocage du réveil est la même.
2. Java AIO est identique à NIO et les méthodes d'implémentation sous-jacentes de chaque plate-forme sont également différentes.EPoll est utilisé sous Linux, IOCP est utilisé sous Windows et KQueue est utilisé sous Mac OS. Le principe est le même, tous nécessitent un thread utilisateur pour bloquer et attendre les événements IO, et un pool de threads pour traiter les événements de la file d'attente.
3. La raison pour laquelle Netty a supprimé AIO est que AIO n'est pas supérieur à NIO en termes de performances. Bien que Linux dispose également d'un ensemble d'implémentations AIO natives (similaire à IOCP sous Windows), Java AIO n'est pas utilisé sous Linux, mais est implémenté avec EPoll.
4. Java AIO ne prend pas en charge UDP
5. La méthode de programmation AIO est légèrement compliquée, comme le "rappel de la mort"