Die folgende Tabelle fasst die Hauptunterschiede zwischen Java NIO und IO zusammen. Ich werde die Unterschiede in jedem Teil der Tabelle detaillierter beschreiben.
Kopieren Sie den Codecode wie folgt:
IO NIO
Streamorientiert und pufferorientiert
Blockierende E/A. Nicht blockierende E/A
Kein Selektor
Streamorientiert und pufferorientiert
Der erste große Unterschied zwischen Java NIO und IO besteht darin, dass IO streamorientiert und NIO pufferorientiert ist. Java IO ist Stream-orientiert, was bedeutet, dass ein oder mehrere Bytes gleichzeitig aus dem Stream gelesen werden und bis alle Bytes gelesen sind, werden sie nirgendwo zwischengespeichert. Darüber hinaus können Daten im Stream nicht vorwärts oder rückwärts verschoben werden. Wenn Sie die aus dem Stream gelesenen Daten hin und her verschieben müssen, müssen Sie sie zunächst in einem Puffer zwischenspeichern. Der pufferorientierte Ansatz von Java NIO ist etwas anders. Die Daten werden in einen Puffer eingelesen, den sie später verarbeiten und bei Bedarf im Puffer hin und her bewegen. Dies erhöht die Flexibilität in der Verarbeitung. Sie müssen jedoch auch überprüfen, ob der Puffer alle Daten enthält, die Sie verarbeiten müssen. Stellen Sie außerdem sicher, dass unverarbeitete Daten im Puffer nicht überschrieben werden, wenn mehr Daten in den Puffer eingelesen werden.
Blockierende und nicht blockierende E/A
Verschiedene Streams von Java IO blockieren. Das bedeutet, dass, wenn ein Thread read() oder write() aufruft, der Thread blockiert wird, bis einige Daten gelesen oder die Daten vollständig geschrieben wurden. Während dieser Zeit kann der Thread nichts anderes tun. Der nicht blockierende Modus von Java NIO ermöglicht es einem Thread, eine Anfrage zum Lesen von Daten von einem bestimmten Kanal zu senden, kann jedoch nur die aktuell verfügbaren Daten abrufen. Wenn derzeit keine Daten verfügbar sind, werden keine Daten abgerufen. Anstatt den Thread blockiert zu halten, kann der Thread weiterhin andere Dinge tun, bis die Daten lesbar werden. Das Gleiche gilt für nicht blockierende Schreibvorgänge. Ein Thread fordert das Schreiben einiger Daten in einen Kanal an, muss jedoch nicht warten, bis diese vollständig geschrieben sind. Der Thread kann in der Zwischenzeit andere Dinge tun. Threads nutzen normalerweise Leerlaufzeit bei nicht blockierendem E/A, um E/A-Vorgänge auf anderen Kanälen auszuführen, sodass ein einzelner Thread jetzt mehrere Eingabe- und Ausgabekanäle verwalten kann.
Selektoren
Mit den Selektoren von Java NIO kann ein einzelner Thread mehrere Kanäle mit einem Selektor überwachen und dann einen separaten Thread zum „Auswählen“ von Kanälen verwenden: Diese Kanäle verfügen bereits über Eingaben, die verarbeitet werden können bereit zum Schreiben. Dieser Auswahlmechanismus erleichtert einem einzelnen Thread die Verwaltung mehrerer Kanäle.
Wie sich NIO und IO auf das Anwendungsdesign auswirken
Unabhängig davon, ob Sie sich für eine IO- oder NIO-Toolbox entscheiden, gibt es mehrere Aspekte, die sich auf Ihr Anwendungsdesign auswirken können:
1. API-Aufrufe an NIO- oder IO-Klassen.
2. Datenverarbeitung.
3. Die Anzahl der Threads, die zur Datenverarbeitung verwendet werden.
API-Aufruf
Natürlich sehen die API-Aufrufe bei der Verwendung von NIO anders aus als bei der Verwendung von IO, aber das ist nicht unerwartet, denn anstatt nur Byte für Byte aus einem InputStream zu lesen, müssen die Daten zunächst in einen Puffer eingelesen und dann verarbeitet werden.
Datenverarbeitung
Bei Verwendung des reinen NIO-Designs im Vergleich zum IO-Design ist auch die Datenverarbeitung betroffen.
Beim IO-Design lesen wir Daten Byte für Byte aus InputStream oder Reader. Angenommen, Sie verarbeiten einen zeilenbasierten Textdatenstrom, zum Beispiel:
Kopieren Sie den Codecode wie folgt:
Name: Anna
Alter: 25
E-Mail: [email protected]
Telefon: 1234567890
Der Textzeilenstrom kann wie folgt gehandhabt werden:
Kopieren Sie den Codecode wie folgt:
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
String nameLine = reader.readLine();
String ageLine = reader.readLine();
String emailLine = reader.readLine();
String phoneLine = reader.readLine();
Beachten Sie, dass der Verarbeitungsstatus davon abhängt, wie lange das Programm ausgeführt wurde. Mit anderen Worten: Sobald die Methode „reader.readLine()“ zurückkehrt, wissen Sie sicher, dass die Textzeile gelesen wurde. Aus diesem Grund blockiert readline(), bis die gesamte Zeile gelesen wurde. Sie wissen auch, dass diese Zeile Namen enthält. Wenn der zweite readline()-Aufruf zurückkehrt, wissen Sie auch, dass diese Zeile Altersangaben usw. enthält. Wie Sie sehen, wird dieser Handler nur ausgeführt, wenn neue Daten eingelesen werden, und kennt bei jedem Schritt die Daten. Sobald ein laufender Thread einige der gelesenen Daten verarbeitet hat, wird er die Daten (meistens) nicht zurücksetzen. Auch die folgende Abbildung verdeutlicht dieses Prinzip:
(Java IO: Daten aus einem blockierenden Stream lesen) Während eine NIO-Implementierung anders sein wird, ist hier ein einfaches Beispiel:
Kopieren Sie den Code wie folgt: ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
Beachten Sie die zweite Zeile, in der Bytes aus dem Kanal in einen ByteBuffer gelesen werden. Wenn dieser Methodenaufruf zurückkehrt, wissen Sie nicht, ob sich alle benötigten Daten im Puffer befinden. Sie wissen lediglich, dass der Puffer einige Bytes enthält, was die Verarbeitung etwas erschwert.
Angenommen, nach dem ersten Leseaufruf (Puffer) sind die in den Puffer eingelesenen Daten nur eine halbe Zeile, z. B. „Name: An“. Können Sie die Daten verarbeiten? Offensichtlich nicht, Sie müssen warten, bis die gesamte Datenzeile in den Cache eingelesen wurde. Davor ist jede Verarbeitung der Daten bedeutungslos.
Woher wissen Sie also, ob der Puffer genügend Daten zur Verarbeitung enthält? Nun, Sie wissen es nicht. Erkannte Methoden können nur Daten im Puffer anzeigen. Dies führt dazu, dass Sie die Daten des Puffers mehrmals überprüfen müssen, bevor Sie wissen, dass sich alle Daten im Puffer befinden. Dies ist nicht nur ineffizient, sondern kann auch die Programmierlösung überladen. Zum Beispiel:
Kopieren Sie den Codecode wie folgt:
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
while(! bufferFull(bytesRead) ) {
bytesRead = inChannel.read(buffer);
}
Die Methode bufferFull() muss verfolgen, wie viele Daten in den Puffer eingelesen wurden, und je nachdem, ob der Puffer voll ist, true oder false zurückgeben. Mit anderen Worten: Wenn der Puffer zur Verarbeitung bereit ist, ist er voll.
Die Methode bufferFull() durchsucht den Puffer, muss jedoch im gleichen Zustand bleiben wie vor dem Aufruf der Methode bufferFull(). Andernfalls werden die nächsten in den Puffer eingelesenen Daten möglicherweise nicht an der richtigen Stelle gelesen. Das ist unmöglich, aber es ist ein weiteres Problem, dessen man sich bewusst sein muss.
Wenn der Puffer voll ist, kann er verarbeitet werden. Wenn es nicht funktioniert und es in Ihrem konkreten Fall sinnvoll ist, können Sie möglicherweise einiges davon in den Griff bekommen. Doch in vielen Fällen ist dies nicht der Fall. Die folgende Abbildung zeigt „Pufferdatenzyklus bereit“:
3) Anzahl der Threads, die zur Datenverarbeitung verwendet werden
Mit NIO können Sie mehrere Kanäle (Netzwerkverbindungen oder Dateien) mit nur einem einzigen Thread (oder einigen wenigen) verwalten. Der Nachteil besteht jedoch darin, dass das Parsen der Daten komplexer sein kann als das Lesen aus einem blockierenden Stream.
Wenn Sie Tausende von gleichzeitig geöffneten Verbindungen verwalten müssen, die jedes Mal nur kleine Datenmengen senden, wie z. B. einen Chat-Server, kann ein Server, der NIO implementiert, von Vorteil sein. Wenn Sie viele offene Verbindungen zu anderen Computern aufrechterhalten müssen, beispielsweise in einem P2P-Netzwerk, kann es ebenfalls von Vorteil sein, einen separaten Thread für die Verwaltung aller ausgehenden Verbindungen zu verwenden. Der Entwurfsplan für mehrere Verbindungen in einem Thread lautet wie folgt:
Java NIO: Einzelner Thread, der mehrere Verbindungen verwaltet
Wenn Sie über eine kleine Anzahl von Verbindungen verfügen, die eine sehr hohe Bandbreite nutzen und große Datenmengen auf einmal senden, könnte eine typische IO-Server-Implementierung möglicherweise eine gute Lösung sein. Die folgende Abbildung zeigt ein typisches IO-Server-Design:
Java IO: Ein typisches IO-Serverdesign – eine Verbindung wird von einem Thread verwaltet