Java NIO (New Input/Output) – das neue Input/Output-API-Paket – wurde 2002 in J2SE 1.4 eingeführt. Das Ziel von Java NIO besteht darin, die Leistung I/O-intensiver Aufgaben auf der Java-Plattform zu verbessern. Zehn Jahre später wissen viele Java-Entwickler immer noch nicht, wie sie NIO vollständig nutzen können, und noch weniger Menschen wissen, dass die aktualisierte Eingabe-/Ausgabe-API (NIO.2) in Java SE 7 eingeführt wurde. Der größte Beitrag von NIO und NIO.2 zur Java-Plattform besteht darin, die Leistung einer Kernkomponente in der Java-Anwendungsentwicklung zu verbessern: der Eingabe-/Ausgabeverarbeitung. Allerdings ist keines der beiden Pakete sehr nützlich und nicht für alle Szenarien geeignet. Bei richtiger Verwendung können Java NIO und NIO.2 den Zeitaufwand für einige gängige E/A-Vorgänge erheblich reduzieren. Das ist die Superkraft von NIO und NIO.2, und in diesem Artikel zeige ich Ihnen 5 einfache Möglichkeiten, sie zu nutzen.
Änderungsbenachrichtigungen (da jedes Ereignis einen Listener erfordert)
Selektoren und asynchrone E/A: Verbesserung des Multiplexings durch Selektoren
Kanal – Versprechen und Realität
Memory Mapping – für die Klinge wird guter Stahl verwendet
Zeichenkodierung und Suche
NIOs Hintergrund
Warum ist ein Erweiterungspaket, das es schon seit 10 Jahren gibt, ein neues I/O-Paket für Java? Der Grund dafür ist, dass für die meisten Java-Programmierer grundlegende E/A-Operationen ausreichend sind. In der täglichen Arbeit müssen die meisten Java-Entwickler NIO nicht lernen. Um noch einen Schritt weiter zu gehen: NIO ist mehr als nur ein Leistungsverbesserungspaket. Stattdessen handelt es sich um eine Sammlung verschiedener Funktionen im Zusammenhang mit Java I/O. NIO erreicht eine Leistungsverbesserung, indem es die Leistung von Java-Anwendungen „näher an das Wesentliche“ bringt, was bedeutet, dass die APIs von NIO und NIO.2 den Zugang zu Systemoperationen auf niedriger Ebene freigeben. Der Preis von NIO besteht darin, dass es zwar leistungsfähigere I/O-Steuerungsfunktionen bietet, uns aber auch eine sorgfältigere Verwendung und Übung als die grundlegende I/O-Programmierung abverlangt. Ein weiteres Merkmal von NIO ist der Fokus auf die Ausdruckskraft der Anwendung, was wir in den folgenden Übungen sehen werden.
Beginnen Sie mit dem Erlernen von NIO und NIO.2
Es gibt viele Referenzmaterialien für NIO – einige ausgewählte Links in den Referenzmaterialien. Zum Erlernen von NIO und NIO.2 sind die Dokumentation der Java 2 SDK Standard Edition (SE) und die Dokumentation der Java SE 7 unverzichtbar. Um den Code in diesem Artikel verwenden zu können, müssen Sie JDK 7 oder höher verwenden.
Viele Entwickler begegnen NIO möglicherweise zum ersten Mal bei der Wartung von Anwendungen: Eine funktionierende Anwendung wird immer langsamer, daher schlagen einige Leute die Verwendung von NIO vor, um die Reaktionsgeschwindigkeit zu verbessern. NIO ist besser, wenn es um die Verbesserung der Anwendungsleistung geht, aber die spezifischen Ergebnisse hängen vom zugrunde liegenden System ab (Beachten Sie, dass NIO plattformabhängig ist). Wenn Sie NIO zum ersten Mal verwenden, müssen Sie es sorgfältig abwägen. Sie werden feststellen, dass die Fähigkeit von NIO, die Leistung zu verbessern, nicht nur vom Betriebssystem abhängt, sondern auch von der von Ihnen verwendeten JVM, dem virtuellen Kontext des Hosts, den Eigenschaften des Massenspeichers und sogar den Daten. Daher ist die Arbeit der Leistungsmessung relativ schwierig durchzuführen. Insbesondere wenn Ihr System über eine tragbare Bereitstellungsumgebung verfügt, müssen Sie besonders darauf achten.
Nachdem wir den obigen Inhalt verstanden haben, machen wir uns keine Sorgen. Lassen Sie uns nun die fünf wichtigen Funktionen von NIO und NIO.2 kennenlernen.
1. Änderungsbenachrichtigung (da jedes Ereignis einen Listener erfordert)
Ein häufiges Anliegen von Entwicklern, die sich für NIO und NIO.2 interessieren, ist die Leistung von Java-Anwendungen. Meiner Erfahrung nach ist der File Change Notifier in NIO.2 das interessanteste (und unterschätzteste) Feature der neuen Eingabe-/Ausgabe-API.
Viele Anwendungen auf Unternehmensebene erfordern in den folgenden Situationen eine spezielle Verarbeitung:
Wenn eine Datei in einen FTP-Ordner hochgeladen wird
Wenn eine Definition in einer Konfiguration geändert wird
Wenn ein Dokumententwurf hochgeladen wird
Wenn andere Dateisystemereignisse auftreten
Dies sind alles Beispiele für Änderungsbenachrichtigungen oder Änderungsreaktionen. In früheren Versionen von Java (und anderen Sprachen) war die Abfrage die beste Möglichkeit, diese Änderungsereignisse zu erkennen. Beim Polling handelt es sich um eine Endlosschleife einer besonderen Art: Überprüfen Sie das Dateisystem oder andere Objekte und vergleichen Sie es mit dem vorherigen Zustand. Wenn sich keine Änderung ergibt, fahren Sie nach einem Intervall von einigen hundert Millisekunden oder 10 Sekunden mit der Überprüfung fort. Dies geschieht in einer Endlosschleife.
NIO.2 bietet eine bessere Möglichkeit zur Änderungserkennung. Listing 1 ist ein einfaches Beispiel.
Listing 1. Änderungsbenachrichtigungsmechanismus in NIO.2
Kopieren Sie den Codecode wie folgt:
import java.nio.file.attribute.*;
importjava.io.*;
importjava.util.*;
importjava.nio.file.Path;
importjava.nio.file.Paths;
importjava.nio.file.StandardWatchEventKinds;
importjava.nio.file.WatchEvent;
importjava.nio.file.WatchKey;
importjava.nio.file.WatchService;
importjava.util.List;
publicclassWatcher{
publicstaticvoidmain(String[]args){
Paththis_dir=Paths.get(".");
System.out.println("Nowwatchingthecurrentdirectory...");
versuchen{
WatchServicewatcher=this_dir.getFileSystem().newWatchService();
this_dir.register(watcher,StandardWatchEventKinds.ENTRY_CREATE);
WatchKeywatckKey=watcher.take();
List<WatchEvent<<64;>>events=watckKey.pollEvents();
for(WatchEventevent:events){
System.out.println("Someonejustcreatedthefile'"+event.context().toString()+"'.");
}
}catch(Ausnahme){
System.out.println("Fehler:"+e.toString());
}
}
}
Kompilieren Sie diesen Code und führen Sie ihn über die Befehlszeile aus. Erstellen Sie im selben Verzeichnis eine neue Datei, führen Sie beispielsweise den Befehl touchexample oder copyWatcher.classexample aus. Sie sehen die folgende Änderungsbenachrichtigung:
Jemand hat einfach das Beispiel „Beispiel 1“ erstellt.
Dieses einfache Beispiel zeigt, wie Sie mit der Nutzung der JavaNIO-Funktionalität beginnen. Gleichzeitig wird auch die NIO.2 Watcher-Klasse eingeführt, die direkter und einfacher zu verwenden ist als das Abfrageschema in der ursprünglichen E/A.
Achten Sie auf Rechtschreibfehler
Achten Sie beim Kopieren des Codes aus diesem Artikel auf Rechtschreibfehler. Beispielsweise liegt das StandardWatchEventKinds-Objekt in Listing 1 im Plural vor. Sogar in der Java.net-Dokumentation ist es falsch geschrieben.
Tipps
Der Benachrichtigungsmechanismus in NIO ist einfacher zu verwenden als die alte Abfragemethode, was dazu führt, dass Sie die detaillierte Analyse spezifischer Anforderungen ignorieren müssen. Wenn Sie einen Listener zum ersten Mal verwenden, müssen Sie die Semantik der von Ihnen verwendeten Konzepte sorgfältig prüfen. Beispielsweise ist es wichtiger zu wissen, wann eine Änderung endet, als zu wissen, wann sie beginnt. Diese Art der Analyse muss sehr sorgfältig durchgeführt werden, insbesondere bei häufigen Szenarien wie dem Verschieben von FTP-Ordnern. NIO ist ein sehr leistungsfähiges Paket, weist jedoch auch einige subtile „Fallstricke“ auf, die bei denjenigen, die damit nicht vertraut sind, Verwirrung stiften können.
2. Selektoren und asynchrone E/A: Verbessern Sie das Multiplexing durch Selektoren
NIO-Neulinge assoziieren es im Allgemeinen mit „nicht blockierender Ein-/Ausgabe“. NIO ist mehr als nur nicht blockierende E/A, aber diese Wahrnehmung ist nicht ganz falsch: Javas grundlegendes E/A blockiert E/A – das heißt, es wartet, bis der Vorgang abgeschlossen ist – jedoch nicht blockierende oder asynchrone E/A ist die am häufigsten verwendete Funktion von NIO, nicht alle von NIO.
Die nicht blockierende E/A von NIO ist ereignisgesteuert und wird im Dateisystem-Listening-Beispiel in Listing 1 demonstriert. Das bedeutet, einen Selektor (Callback oder Listener) für einen I/O-Kanal zu definieren, und dann kann das Programm weiterlaufen. Wenn auf diesem Selektor ein Ereignis auftritt – beispielsweise der Empfang einer Eingabezeile – „wacht“ der Selektor auf und wird ausgeführt. All dies wird über einen einzigen Thread implementiert, der sich erheblich von der Standard-E/A von Java unterscheidet.
Listing 2 zeigt ein Multi-Port-Netzwerkprogramm echo-er, das mit dem Selektor von NIO implementiert wurde. Dies ist eine Modifikation eines kleinen Programms, das 2003 von Greg Travis erstellt wurde (siehe Ressourcenliste). Unix und Unix-ähnliche Systeme verfügen seit langem über effiziente Selektoren, die ein gutes Referenzmodell für leistungsstarke Programmiermodelle in Java-Netzwerken darstellen.
Listing 2.NIO-Selektor
Kopieren Sie den Codecode wie folgt:
importjava.io.*;
importjava.net.*;
importjava.nio.*;
importjava.nio.channels.*;
importjava.util.*;
publicclassMultiPortEcho
{
privateintports[];
privateByteBufferechoBuffer=ByteBuffer.allocate(1024);
publicMultiPortEcho(intports[])throwsIOException{
this.ports=ports;
configure_selector();
}
privatevoidconfigure_selector()throwsIOException{
//Erstelle einen neuen Selektor
Selectorselector=Selector.open();
//Openalisteneroneachport,andregistereachone
//withtheselector
for(inti=0;i<ports.length;++i){
ServerSocketChannelssc=ServerSocketChannel.open();
ssc.configureBlocking(false);
ServerSocketss=ssc.socket();
InetSocketAddressaddress=newInetSocketAddress(ports[i]);
ss.bind(Adresse);
SelectionKeykey=ssc.register(selector,SelectionKey.OP_ACCEPT);
System.out.println("Goingtolistenon"+ports[i]);
}
while(true){
intnum=selector.select();
SetselectedKeys=selector.selectedKeys();
Iteratorit=selectedKeys.iterator();
while(it.hasNext()){
SelectionKeykey=(SelectionKey)it.next();
if((key.readyOps()&SelectionKey.OP_ACCEPT)
==SelectionKey.OP_ACCEPT){
//Akzeptiere die neue Verbindung
ServerSocketChannelssc=(ServerSocketChannel)key.channel();
SocketChannelsc=ssc.accept();
sc.configureBlocking(false);
//Füge die neue Verbindung zum Selektor hinzu
SelectionKeynewKey=sc.register(selector,SelectionKey.OP_READ);
it.remove();
System.out.println("Gotconnectionfrom"+sc);
}elseif((key.readyOps()&SelectionKey.OP_READ)
==SelectionKey.OP_READ){
//Daten lesen
SocketChannelsc=(SocketChannel)key.channel();
//Echodaten
intbytesEchoed=0;
while(true){
echoBuffer.clear();
intnumber_of_bytes=sc.read(echoBuffer);
if(number_of_bytes<=0){
brechen;
}
echoBuffer.flip();
sc.write(echoBuffer);
bytesEchoed+=number_of_bytes;
}
System.out.println("Echoed"+bytesEchoed+"from"+sc);
it.remove();
}
}
}
}
staticpublicvoidmain(Stringargs[])throwsException{
if(args.length<=0){
System.err.println("Usage:javaMultiPortEchoport[portport...]");
System.exit(1);
}
intports[]=newint[args.length];
for(inti=0;i<args.length;++i){
ports[i]=Integer.parseInt(args[i]);
}
newMultiPortEcho(ports);
}
}
Kompilieren Sie diesen Code und starten Sie ihn mit einem Befehl wie javaMultiPortEcho80058006. Sobald dieses Programm erfolgreich ausgeführt wird, starten Sie ein einfaches Telnet oder einen anderen Terminalemulator, um eine Verbindung zu den 8005- und 8006-Schnittstellen herzustellen. Sie werden sehen, dass dieses Programm alle empfangenen Zeichen wiedergibt – und zwar über einen Java-Thread.
3. Passage: Versprechen und Realität
In NIO kann ein Kanal jedes Objekt darstellen, das gelesen und geschrieben werden kann. Seine Aufgabe besteht darin, eine Abstraktion für Dateien und Sockets bereitzustellen. NIO-Kanäle unterstützen einen konsistenten Satz von Methoden, sodass Sie bei der Kodierung nicht besonders auf unterschiedliche Objekte achten müssen, unabhängig davon, ob es sich um eine Standardausgabe, eine Netzwerkverbindung oder den verwendeten Kanal handelt. Diese Funktion von Kanälen wird von Streams in Java Basic I/O geerbt. Streams bieten blockierende E/A; Kanäle unterstützen asynchrone E/A.
NIO wird oft wegen seiner hohen Leistung empfohlen, genauer gesagt aber wegen seiner schnellen Reaktionszeiten. In einigen Szenarien ist die Leistung von NIO schlechter als die der einfachen Java-E/A. Beispielsweise kann bei einem einfachen sequentiellen Lesen und Schreiben einer kleinen Datei die Leistung, die allein durch Streaming erreicht wird, zwei- bis dreimal schneller sein als bei der entsprechenden ereignisorientierten kanalbasierten Codierungsimplementierung. Gleichzeitig sind Nicht-Multiplex-Kanäle – also ein separater Kanal pro Thread – viel langsamer als mehrere Kanäle, die ihre Selektoren im selben Thread registrieren.
Wenn Sie nun darüber nachdenken, ob Sie Streams oder Kanäle nutzen möchten, stellen Sie sich die folgenden Fragen:
Wie viele I/O-Objekte müssen Sie lesen und schreiben?
Sind verschiedene E/A-Objekte sequentiell oder müssen sie alle gleichzeitig ausgeführt werden?
Müssen Ihre I/O-Objekte nur für einen kurzen Zeitraum oder für die gesamte Lebensdauer Ihres Prozesses bestehen bleiben?
Ist Ihr I/O für die Verarbeitung in einem einzelnen Thread oder in mehreren verschiedenen Threads geeignet?
Sehen Netzwerkkommunikation und lokale E/A gleich aus oder weisen sie unterschiedliche Muster auf?
Eine solche Analyse ist eine bewährte Vorgehensweise bei der Entscheidung, ob Streams oder Kanäle verwendet werden sollen. Denken Sie daran: NIO und NIO.2 sind kein Ersatz für Basis-I/O, sondern eine Ergänzung dazu.
4. Memory Mapping – für die Klinge wird guter Stahl verwendet
Die bedeutendste Leistungsverbesserung bei NIO ist die Speicherzuordnung. Bei der Speicherzuordnung handelt es sich um einen Dienst auf Systemebene, der einen Abschnitt einer in einem Programm verwendeten Datei als Speicher behandelt.
Es gibt viele potenzielle Auswirkungen der Speicherzuordnung, mehr als ich hier angeben kann. Auf einer höheren Ebene kann die E/A-Leistung des Dateizugriffs die Geschwindigkeit des Speicherzugriffs erreichen. Der Speicherzugriff ist oft um Größenordnungen schneller als der Dateizugriff. Listing 3 ist ein einfaches Beispiel für eine NIO-Speicherzuordnung.
Listing 3. Speicherzuordnung in NIO
Kopieren Sie den Codecode wie folgt:
importjava.io.RandomAccessFile;
importjava.nio.MappedByteBuffer;
importjava.nio.channels.FileChannel;
publicclassmem_map_example{
privatestaticintmem_map_size=20*1024*1024;
privatestaticStringfn="example_memory_mapped_file.txt";
publicstaticvoidmain(String[]args)throwsException{
RandomAccessFilememoryMappedFile=newRandomAccessFile(fn,"rw");
//Mappingafileintomemory
MappedByteBufferout=memoryMappedFile.getChannel().map(FileChannel.MapMode.READ_WRITE,0,mem_map_size);
//SchreibenintoMemoryMappedFile
for(inti=0;i<mem_map_size;i++){
out.put((byte)'A');
}
System.out.println("File'"+fn+"'isnow"+Integer.toString(mem_map_size)+"bytesfull.");
//Readfrommemory-mappedfile.
for(inti=0;i<30;i++){
System.out.print((char)out.get(i));
}
System.out.println("/nReadingfrommemory-mappedfile'"+fn+"'iscomplete.");
}
}
In Listing 3 erstellt dieses einfache Beispiel eine 20 MB große Datei example_memory_mapped_file.txt, füllt sie mit dem Zeichen A und liest dann die ersten 30 Bytes. In praktischen Anwendungen verbessert die Speicherzuordnung nicht nur die reine E/A-Geschwindigkeit, sondern ermöglicht auch die gleichzeitige Verarbeitung desselben Dateibilds durch mehrere verschiedene Lese- und Schreibvorgänge. Diese Technologie ist leistungsstark, aber auch gefährlich, aber bei richtiger Anwendung erhöht sie Ihre IO-Geschwindigkeit um ein Vielfaches. Wie wir alle wissen, nutzen Wall-Street-Handelsgeschäfte die Memory-Mapping-Technologie, um innerhalb von Sekunden oder sogar Millisekunden Vorteile zu erzielen.
5. Zeichenkodierung und Suche
Die letzte Funktion von NIO, die ich in diesem Artikel erklären möchte, ist charset, ein Paket, das zum Konvertieren verschiedener Zeichenkodierungen verwendet wird. Vor NIO implementierte Java die meisten der gleichen Funktionen, die über die getByte-Methode integriert waren. Charset ist beliebt, weil es flexibler als getBytes ist und auf einer niedrigeren Ebene implementiert werden kann, was zu einer besseren Leistung führt. Dies ist umso wertvoller für die Suche nach nicht-englischen Sprachen, die empfindlich auf Codierung, Reihenfolge und andere Sprachmerkmale reagieren.
Listing 4 zeigt ein Beispiel für die Konvertierung von Unicode-Zeichen in Java in Latin-1
Liste 4. Zeichen in NIO
Kopieren Sie den Codecode wie folgt:
Stringsome_string="Dies ist ein String, der Unicode in Java speichert.";
Charsetlatin1_charset=Charset.forName("ISO-8859-1");
CharsetEncodelatin1_encoder=charset.newEncoder();
ByteBufferlatin1_bbuf=latin1_encoder.encode(CharBuffer.wrap(some_string));
Beachten Sie, dass Zeichensätze und Kanäle für die gemeinsame Verwendung konzipiert sind, sodass das Programm normal ausgeführt werden kann, wenn Speicherzuordnung, asynchrone E/A und Kodierungskonvertierung koordiniert werden.
Zusammenfassung: Natürlich gibt es noch mehr zu wissen
Der Zweck dieses Artikels besteht darin, Java-Entwickler mit einigen der wichtigsten (und nützlichsten) Funktionen in NIO und NIO.2 vertraut zu machen. Sie können die durch diese Beispiele geschaffenen Grundlagen nutzen, um einige andere Methoden von NIO zu verstehen. Das Wissen, das Sie über Kanäle lernen, kann Ihnen beispielsweise dabei helfen, die Verarbeitung symbolischer Links im Dateisystem im NIO-Pfad zu verstehen. Sie können auch auf die Ressourcenliste zurückgreifen, die ich später gegeben habe. Sie enthält einige Dokumente für eine eingehende Untersuchung der neuen E/A-API von Java.