1. Einleitung
Es gibt viele solcher Artikel über die Unterschiede und Prinzipien von Java BIO, NIO und AIO, aber sie werden hauptsächlich zwischen BIO und NIO diskutiert, während es nur sehr wenige Artikel über AIO gibt und viele davon nur Einführungen sind die Konzepte und Codebeispiele.
Beim Kennenlernen von AIO sind folgende Phänomene aufgefallen:
1. Java 7 wurde 2011 veröffentlicht, das ein Programmiermodell namens AIO namens asynchronous IO hinzufügte, aber fast 12 Jahre sind vergangen, und die übliche Entwicklungsframework-Middleware wird immer noch von NIO dominiert, wie z. B. das Netzwerkframework Netty, Mina, Webcontainer Kater, Sog.
2. Java AIO wird auch NIO 2.0 genannt, basiert es ebenfalls auf NIO?
3. Netty stellte die Unterstützung von AIO ein. https://github.com/netty/netty/issues/2515
4. AIO scheint nur das Problem gelöst und eine Einsamkeit befreit zu haben.
Diese Phänomene werden unweigerlich viele Menschen verwirren. Als ich mich entschied, diesen Artikel zu schreiben, wollte ich nicht einfach das Konzept von AIO wiederholen, sondern wie man die Essenz von Java AIO durch das Phänomen analysiert, denkt und versteht.
2. Was ist asynchron
2.1 Asynchronie, wie wir sie kennen
Das A von AIO bedeutet Asynchron. Bevor wir das Prinzip von AIO verstehen, wollen wir klären, was für ein Konzept "asynchron" ist.
Apropos asynchrone Programmierung, sie ist in der normalen Entwicklung noch relativ verbreitet, wie die folgenden Codebeispiele:
@Async
public void create() {
//TODO
}
public void build() {
executor.execute(() -> build());
}
Unabhängig davon, ob es mit @Async annotiert ist oder Aufgaben an den Thread-Pool übermittelt werden, enden alle mit dem gleichen Ergebnis, nämlich die Übergabe der auszuführenden Aufgabe an einen anderen Thread zur Ausführung.
Zu diesem Zeitpunkt kann grob davon ausgegangen werden, dass das sogenannte "asynchrone" Multithreading ist und Aufgaben ausführt.
2.2 Sind Java BIO und NIO synchron oder asynchron?
Ob Java BIO und NIO synchron oder asynchron sind, wir programmieren zunächst asynchron nach der Idee der Asynchronität.
2.2.1 BIO-Beispiel
byte [] data = new byte[1024];
InputStream in = socket.getInputStream();
in.read(data);
// 接收到数据,异步处理
executor.execute(() -> handle(data));
public void handle(byte [] data) {
// TODO
}
Bei BIO read() kann, obwohl der Thread blockiert ist, beim Empfangen von Daten ein Thread zur Verarbeitung asynchron gestartet werden.
2.2.2 NIO-Beispiel
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
}
Auf die gleiche Weise kann NIO read(), obwohl es nicht blockiert, das Warten auf Daten durch select() blockieren. Wenn Daten zu lesen sind, startet es asynchron einen Thread, um Daten zu lesen und zu verarbeiten.
2.2.3 Abweichungen im Verständnis
Zu diesem Zeitpunkt schwören wir, dass es von Ihrer Stimmung abhängt, ob BIO und NIO von Java asynchron oder synchron sind.Wenn Sie ihm gerne einen Multi-Thread geben, ist es asynchron.
Aber wenn dies der Fall ist, ist nach dem Lesen vieler Blogartikel grundsätzlich klargestellt, dass BIO und NIO synchronisiert sind.
Wo liegt also das Problem, was hat die Abweichung in unserem Verständnis verursacht?
Das ist das Problem des Bezugsrahmens: Ob sich die Fahrgäste im Bus bewegen oder stehen, wenn man zuvor Physik studiert hat, braucht man einen Bezugsrahmen: Wenn man den Boden als Bezugspunkt nimmt, bewegt er sich, und der Bus wird als Bezugsrahmen benutzt eine Referenz, er ist stationär.
Dasselbe gilt für Java IO. Ein Referenzsystem wird benötigt, um zu definieren, ob es synchron oder asynchron ist. Da wir diskutieren, welcher Modus von IO ist, ist es notwendig, die IO-Lese- und Schreiboperationen zu verstehen, während andere eine andere starten. Threads zum Verarbeiten von Daten sind bereits außerhalb des Bereichs des IO-Lesens und -Schreibens und sollten nicht beteiligt sein.
2.2.4 Der Versuch, async zu definieren
Wenn wir daher das Ereignis von IO-Lese- und -Schreiboperationen als Referenz nehmen, versuchen wir zunächst, den Thread zu definieren, der IO-Lese- und -Schreiboperationen initiiert (der Thread, der Lesen und Schreiben aufruft), und den Thread, der tatsächlich IO-Lese- und -Schreiboperationen durchführt Sie sind derselbe Thread, dann nennen Sie es synchron, andernfalls asynchron .
-
Offensichtlich kann BIO nur synchron sein.Der Aufruf von in.read() blockiert den aktuellen Thread.Wenn Daten zurückgegeben werden, erhält der ursprüngliche Thread die Daten.
-
Und NIO wird auch Synchronisation genannt, und der Grund ist derselbe: Beim Aufruf von channel.read() blockiert der Thread zwar nicht, aber es ist immer noch der aktuelle Thread, der die Daten liest.
Gemäß dieser Idee sollte AIO der Thread sein, der das IO-Lesen und Schreiben initiiert, und der Thread, der die Daten tatsächlich empfängt, muss möglicherweise nicht derselbe Thread sein. Ist
dies der Fall? Lassen Sie uns jetzt mit dem Java-AIO-Code beginnen.
2.3 Java-AIO-Programmbeispiel
2.3.1 AIO-Serverprogramm
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 AIO-Client-Programm
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 Asynchrone Definitionsvermutung Schlussfolgerung
Führen Sie die Server- und Clientprogramme separat aus
Als Ergebnis des Ausführens des Servers
Der Hauptthread initiiert einen Aufruf von serverChannel.accept und fügt einen CompletionHandler hinzu, um den Rückruf zu überwachen.Wenn ein Client eine Verbindung herstellt, führt der Thread-5-Thread die abgeschlossene Rückrufmethode von accept aus.
Unmittelbar danach initiierte Thread-5 den clientChannel.read-Aufruf und fügte einen CompletionHandler hinzu, um den Callback zu überwachen.Beim Empfangen von Daten führte Thread-1 die abgeschlossene Callback-Methode von read aus.
Diese Schlussfolgerung stimmt mit der obigen asynchronen Vermutung überein.Der Thread, der die IO-Operation initiiert (z. B. Accept, Read, Write) ist nichtderselbe wie der Thread, der die Operation schließlich abschließt.Wir nennen diesen IO-Modus AIO .
Die Definition von AIO auf diese Weise dient natürlich nur unserem Verständnis, in der Praxis mag die Definition von asynchronem IO abstrakter sein.
3. Das AIO-Beispiel fordert zum Nachdenken auf
1. Wer hat den Thread erstellt, der die Methode complete() ausführt, und wann wurde er erstellt?
2. Wie implementiert man die AIO-Registrierungsereignisüberwachung und den Ausführungs-Callback?
3. Was ist die Essenz der Rückrufüberwachung?
3.1 Frage 1: Wer hat den Thread erstellt, der die Methode complete() ausführt, und wann wurde er erstellt?
Im Allgemeinen muss ein solches Problem vom Eingang des Programms aus verstanden werden, aber es hängt mit dem Thread zusammen.Tatsächlich ist es möglich, anhand des Laufstatus des Thread-Stapels zu lokalisieren, wie der Thread läuft.
Führen Sie nur das AIO-Serverprogramm aus, der Client wird nicht ausgeführt, drucken Sie den Thread-Stack (Hinweis: Das Programm läuft auf der Linux-Plattform und andere Plattformen sind etwas anders).
Analysieren Sie den Thread-Stack und stellen Sie fest, dass das Programm so viele Threads startet
1. Thread Thread-0 wird in der Methode EPoll.wait() blockiert
2. Faden Faden-1, Faden-2. . . Thread-n (n ist gleich der Anzahl der CPU-Kerne) nimmt () Aufgaben aus der Sperrwarteschlange und blockiert, während er auf die Rückgabe einer Aufgabe wartet.
An dieser Stelle kann vorläufig die nächste Schlussfolgerung gezogen werden:
Nachdem das AIO-Serverprogramm gestartet wurde, werden diese Threads erstellt, und die Threads befinden sich alle in einem blockierten Wartezustand.
Außerdem habe ich festgestellt, dass das Ausführen dieser Threads mit Epoll zusammenhängt. Wenn es um Epoll geht, haben wir den Eindruck, dass Java NIO mit Epoll am unteren Ende der Linux-Plattform implementiert wird. Ist Java AIO auch mit Epoll implementiert ? Um diese Schlussfolgerung zu bestätigen, diskutieren wir ab der nächsten Frage
3.2 Frage 2: Wie implementiert man die AIO-Registrierungsereignisüberwachung und den Ausführungsrückruf
Angesichts dieses Problems stellte ich beim Lesen und Analysieren des Quellcodes fest, dass der Quellcode sehr lang ist und das Analysieren des Quellcodes ein langweiliger Prozess ist, der die Leser leicht vertreiben kann.
Für das Verständnis von langem Prozess und logisch komplexem Code können wir seine verschiedenen Kontexte erfassen und herausfinden, welche Kernprozesse es sind.
Nehmen Sie den Registrierungs-Listener read als Beispiel clientChannel.read(…), sein Hauptkernprozess ist:
1. Ereignis registrieren -> 2. Ereignis abhören -> 3. Ereignis verarbeiten
3.2.1 1. Registrierungsveranstaltung
Das Registrierungsereignis ruft die Funktion EPoll.ctl(…) auf, und der letzte Parameter dieser Funktion wird verwendet, um anzugeben, ob sie einmalig oder dauerhaft ist. Die obigen Codeereignisse | EPOLLONSHOT bedeutet wörtlich, dass es sich um eine einmalige Aktion handelt.
3.2.2 2. Ereignisse überwachen
3.2.3 3. Umgang mit Ereignissen
3.2.4 Zusammenfassung der Kernprozesse
Nachdem Sie den obigen Codefluss analysiert haben, werden Sie feststellen, dass die drei Ereignisse, die für jeden IO-Lese- und Schreibvorgang auftreten müssen, einmalig sind, das heißt, nachdem das Ereignis verarbeitet wurde, ist dieser Prozess beendet. Wenn Sie mit dem nächsten fortfahren möchten IO Zum Lesen und Schreiben müssen Sie wieder von vorne beginnen. Auf diese Weise kommt es zu einem sogenannten Todes-Callback (die nächste Callback-Methode wird der Callback-Methode hinzugefügt), was die Komplexität der Programmierung stark erhöht.
3.3 Frage 3: Worum geht es bei der Überwachung von Rückrufen?
Lassen Sie mich zunächst auf das Fazit eingehen: Die Essenz des sogenannten Monitoring-Callbacks ist der User-Mode-Thread, der die Kernel-Mode-Funktion (genauer gesagt API, wie etwa read, write, epollWait) aufruft, wenn die Funktion hat nicht zurückgegeben, wird der Benutzer-Thread blockiert. Wenn die Funktion zurückkehrt, wird der blockierte Thread aufgeweckt und die sogenannte Callback-Funktion ausgeführt .
Um diese Schlussfolgerung zu verstehen, müssen wir zunächst einige Konzepte einführen
3.3.1 Systemaufrufe und Funktionsaufrufe
Funktionsaufruf:
Suchen Sie eine Funktion und führen Sie zugehörige Befehle in der Funktion aus
Systemaufruf:
Das Betriebssystem stellt Benutzeranwendungen eine Programmierschnittstelle zur Verfügung, die sogenannte API.
Ausführungsprozess des Systemaufrufs:
1. Systemaufrufparameter übergeben
2. Führen Sie getrapte Anweisungen aus, wechseln Sie vom Benutzermodus in den Core-Modus, da Systemaufrufe im Allgemeinen im Core-Modus ausgeführt werden müssen
3. Führen Sie das Systemaufrufprogramm aus
4. Zurück zum Benutzerstatus
3.3.2 Kommunikation zwischen Benutzermodus und Kernelmodus
Benutzermodus -> Kernelmodus, nur durch Systemaufrufe.
Kernelmodus -> Benutzermodus, der Kernelmodus weiß nicht, welche Funktionen das Benutzermodusprogramm hat, was die Parameter sind und wo die Adresse ist. Daher ist es für den Kernel unmöglich, Funktionen im Benutzermodus aufzurufen, sondern nur durch das Senden von Signalen.Beispielsweise soll der Kill-Befehl zum Beenden des Programms das Benutzerprogramm durch das Senden von Signalen ordnungsgemäß beenden.
Da es dem Kernel-State unmöglich ist, aktiv Funktionen im User-State aufzurufen, warum gibt es einen Callback, es kann nur gesagt werden, dass dieser sogenannte Callback eigentlich ein selbstgesteuerter und selbstdurchgeführter User-State ist. Es überwacht nicht nur, sondern führt auch die Callback-Funktion aus.
3.3.3 Überprüfen Sie die Schlussfolgerung mit praktischen Beispielen
Um zu überprüfen, ob diese Schlussfolgerung überzeugend ist, hört beispielsweise IntelliJ IDEA, das normalerweise zum Entwickeln und Schreiben von Code verwendet wird, auf Maus- und Tastaturereignisse und verarbeitet Ereignisse.
Drucken Sie gemäß der Konvention zuerst den Thread-Stack aus, und Sie werden feststellen, dass der Thread "AWT-XAWT" für die Überwachung von Ereignissen wie Maus und Tastatur und der Thread "AWT-EventQueue" für die Ereignisverarbeitung verantwortlich ist.
Wenn Sie den spezifischen Code suchen, können Sie sehen, dass "AWT-XAWT" eine While-Schleife ausführt und die Funktion waitForEvents aufruft, um auf die Rückkehr des Ereignisses zu warten. Wenn es kein Ereignis gibt, wurde der Thread dort blockiert.
4. Was ist die Essenz von Java AIO?
1. Da der Kernelmodus Benutzermodusfunktionen nicht direkt aufrufen kann, besteht die Essenz von Java AIO darin, Asynchronität nur im Benutzermodus zu implementieren. Es wird keine Asynchronität im idealen Sinne erreicht.
ideal asynchron
Was ist Asynchronität im idealen Sinne? Hier ist ein Beispiel für Online-Shopping
Zwei Rollen, Verbraucher A und Kurier B
-
Wenn A online einkauft, geben Sie die Privatadresse ein, um zu bezahlen, und senden Sie die Bestellung ab, was der Registrierung des Überwachungsereignisses entspricht
-
Der Händler liefert die Waren und B liefert den Artikel an die Haustür von A, was einem Rückruf gleichkommt.
Nachdem A die Bestellung online aufgegeben hat, braucht er sich um den weiteren Lieferprozess nicht zu kümmern und kann sich anderen Dingen widmen. B ist es egal, ob A zu Hause ist oder nicht, wenn er die Ware liefert. Schmeißen Sie die Ware trotzdem einfach vor die Haustür. Die beiden Personen sind nicht aufeinander angewiesen und stören sich nicht gegenseitig .
Unter der Annahme, dass der Einkauf von A im Benutzermodus erfolgt und die Expresslieferung von B im Kernelmodus erfolgt, ist diese Art von Programmbetriebsmodus zu ideal und kann in der Praxis nicht realisiert werden.
Asynchronität in der Realität
A lebt in einem gehobenen Wohngebiet und kann nicht beliebig eintreten, und der Kurier kann nur bis zum Tor des Wohngebiets geliefert werden.
A kaufte ein relativ schweres Produkt, wie einen Fernseher, weil A zur Arbeit ging und nicht zu Hause war, also bat er einen Freund C, ihm zu helfen, den Fernseher zu sich nach Hause zu bringen.
Bevor A zur Arbeit geht, begrüßt er den Wachmann D an der Tür und sagt, dass heute ein Fernseher geliefert wird.Wenn er am Tor der Gemeinde geliefert wird, rufen Sie bitte C an und bitten ihn, ihn abzuholen.
-
An diesem Punkt gibt A eine Bestellung auf und begrüßt D, was der Registrierung eines Ereignisses gleichkommt. In AIO ist es das Registrierungsereignis EPoll.ctl(...).
-
Der Sicherheitsmann, der an der Tür hockt, ist gleichbedeutend mit dem Abhören des Ereignisses. In AIO ist es der Thread-0-Thread. Do EPoll.wait(…)
-
Der Kurier hat den Fernseher an die Tür geliefert, was der Ankunft eines IO-Ereignisses gleichkommt.
-
Der Wachmann benachrichtigt C, dass der Fernseher angekommen ist, und C kommt, um den Fernseher zu bewegen, was der Bewältigung des Vorfalls gleichkommt.
In AIO sendet Thread-0 Aufgaben an die Aufgabenwarteschlange.
Thread-1 ~n zum Abrufen von Daten und Ausführen der Callback-Methode.
Während des ganzen Vorgangs musste Wachmann D die ganze Zeit in der Hocke sitzen und durfte sich keinen Zentimeter davon entfernen, sonst würde der Fernseher gestohlen, wenn er an die Tür geliefert würde.
Auch Freund C muss bei A bleiben, er ist von jemandem anvertraut, aber die Person ist nicht da, als die Sachen ankommen, das ist ein bisschen unehrlich.
Daher sind die tatsächliche Asynchronität und die ideale Asynchronität unabhängig voneinander und stören sich nicht, diese beiden Punkte sind einander entgegengesetzt . Die Rolle der Sicherheit ist die größte, und dies ist der Höhepunkt seines Lebens.
Das Registrieren von Ereignissen, das Abhören von Ereignissen, das Verarbeiten von Ereignissen und das Aktivieren von Multithreading im asynchronen Prozess, die Initiatoren dieser Prozesse werden alle vom Benutzermodus behandelt, sodass Java AIO nur die Asynchronität im Benutzermodus implementiert, der zuerst mit BIO blockiert wird und NIO , ist das Wesentliche beim Starten der asynchronen Thread-Verarbeitung nach dem Blockieren des Aufweckens dasselbe.
2. Java AIO ist dasselbe wie NIO, und die zugrunde liegenden Implementierungsmethoden jeder Plattform sind ebenfalls unterschiedlich: EPoll wird unter Linux verwendet, IOCP wird unter Windows verwendet und KQueue wird unter Mac OS verwendet. Das Prinzip ist das gleiche, alle erfordern einen Benutzer-Thread zum Blockieren und Warten auf IO-Ereignisse und einen Thread-Pool zum Verarbeiten von Ereignissen aus der Warteschlange.
3. Der Grund, warum Netty AIO entfernt hat, ist, dass AIO in Bezug auf die Leistung nicht höher ist als NIO. Obwohl Linux auch über eine Reihe nativer AIO-Implementierungen verfügt (ähnlich wie IOCP unter Windows), wird Java AIO nicht in Linux verwendet, sondern mit EPoll implementiert.
4. Java AIO unterstützt kein UDP
5. Die AIO-Programmiermethode ist etwas kompliziert, wie z. B. "Death Callback".