Java NIO (New Input/Output) - le nouveau package API d'entrée/sortie - a été introduit dans J2SE 1.4 en 2002. L'objectif de Java NIO est d'améliorer les performances des tâches gourmandes en E/S sur la plateforme Java. Dix ans plus tard, de nombreux développeurs Java ne savent toujours pas comment utiliser pleinement NIO, et encore moins de gens savent que l'API d'entrée/sortie mise à jour (NIO.2) a été introduite dans Java SE 7. La plus grande contribution de NIO et NIO.2 à la plate-forme Java est d'améliorer les performances d'un composant essentiel du développement d'applications Java : le traitement des entrées/sorties. Cependant, aucun des deux packages n’est très utile et ne convient pas à tous les scénarios. S'ils sont utilisés correctement, Java NIO et NIO.2 peuvent réduire considérablement le temps consacré à certaines opérations d'E/S courantes. C'est le super pouvoir de NIO et NIO.2, et dans cet article je vais vous montrer 5 façons simples de les utiliser.
Modifier les notifications (car chaque événement nécessite un auditeur)
Sélecteurs et E/S asynchrones : améliorer le multiplexage via les sélecteurs
Chaîne - Promesse et réalité
Cartographie de la mémoire - un bon acier est utilisé sur la lame
Encodage et recherche de caractères
Le parcours de NIO
Pourquoi un package d'amélioration qui existe depuis 10 ans est-il un nouveau package d'E/S pour Java ? La raison en est que pour la plupart des programmeurs Java, les opérations d'E/S de base sont suffisantes. Dans le travail quotidien, la plupart des développeurs Java n'ont pas besoin d'apprendre NIO. Pour aller plus loin, NIO est plus qu'un simple package d'amélioration des performances. Il s’agit plutôt d’un ensemble de différentes fonctions liées aux E/S Java. NIO améliore les performances en rendant les performances des applications Java « plus proches de l'essence », ce qui signifie que les API de NIO et NIO.2 exposent l'entrée aux opérations système de bas niveau. Le prix de NIO est que, même s'il offre des capacités de contrôle d'E/S plus puissantes, il nous oblige également à utiliser et à pratiquer plus soigneusement que la programmation d'E/S de base. Une autre caractéristique de NIO est l’accent mis sur l’expressivité des applications, ce que nous verrons dans les exercices suivants.
Commencez à apprendre NIO et NIO.2
Il existe de nombreux documents de référence pour NIO - quelques liens sélectionnés dans les documents de référence. Pour apprendre NIO et NIO.2, la documentation Java 2 SDK Standard Edition (SE) et Java SE 7 sont indispensables. Pour utiliser le code de cet article, vous devez utiliser JDK 7 ou supérieur.
Pour de nombreux développeurs, la première fois qu'ils rencontrent NIO peut être lors de la maintenance d'applications : une application qui fonctionne devient de plus en plus lente, c'est pourquoi certaines personnes suggèrent d'utiliser NIO pour améliorer la vitesse de réponse. NIO est plus remarquable lorsqu'il s'agit d'améliorer les performances des applications, mais les résultats spécifiques dépendent du système sous-jacent (notez que NIO dépend de la plate-forme). Si c'est la première fois que vous utilisez NIO, vous devez le peser soigneusement. Vous constaterez que la capacité de NIO à améliorer les performances dépend non seulement du système d'exploitation, mais également de la JVM que vous utilisez, du contexte virtuel de l'hôte, des caractéristiques du stockage de masse et même des données. Le travail de mesure des performances est donc relativement difficile à réaliser. Surtout lorsque votre système dispose d’un environnement de déploiement portable, vous devez y prêter une attention particulière.
Après avoir compris le contenu ci-dessus, nous n'avons plus de soucis. Voyons maintenant les cinq fonctions importantes de NIO et NIO.2.
1. Modifier la notification (car chaque événement nécessite un écouteur)
Une préoccupation commune parmi les développeurs intéressés par NIO et NIO.2 concerne les performances des applications Java. D'après mon expérience, le notificateur de changement de fichier dans NIO.2 est la fonctionnalité la plus intéressante (et sous-estimée) de la nouvelle API d'entrée/sortie.
De nombreuses applications de niveau entreprise nécessitent un traitement spécial dans les situations suivantes :
Lorsqu'un fichier est téléchargé vers un dossier FTP
Lorsqu'une définition dans une configuration est modifiée
Lorsqu'un brouillon de document est téléchargé
Lorsque d'autres événements du système de fichiers se produisent
Ce sont tous des exemples de notifications de modification ou de réponses aux modifications. Dans les versions antérieures de Java (et d'autres langages), l'interrogation était le meilleur moyen de détecter ces événements de modification. L'interrogation est un type particulier de boucle infinie : vérifiez le système de fichiers ou d'autres objets et comparez-le à l'état précédent. S'il n'y a aucun changement, continuez la vérification après un intervalle d'environ quelques centaines de millisecondes ou 10 secondes. Cela se déroule dans une boucle infinie.
NIO.2 offre un meilleur moyen d'effectuer la détection des modifications. Le listing 1 est un exemple simple.
Listing 1. Mécanisme de notification de modification dans NIO.2
Copiez le code comme suit :
importer 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("Je regarde maintenant le répertoire actuel...");
essayer{
WatchServicewatcher=this_dir.getFileSystem().newWatchService();
this_dir.register(watcher,StandardWatchEventKinds.ENTRY_CREATE);
WatchKeywatckKey=watcher.take();
List<WatchEvent<<64;>>events=watckKey.pollEvents();
pour(WatchEventevent:événements){
System.out.println("Quelqu'un vient de créer le fichier'"+event.context().toString()+"'.");
}
}catch(Exception){
System.out.println("Erreur :"+e.toString());
}
}
}
Compilez ce code et exécutez-le depuis la ligne de commande. Dans le même répertoire, créez un nouveau fichier, par exemple, exécutez la commande touchexample ou copyWatcher.classexample. Vous verrez le message de notification de modification suivant :
Quelqu'un vient de créer le champ 'exemple1'.
Cet exemple simple montre comment commencer à utiliser la fonctionnalité JavaNIO. Dans le même temps, il introduit également la classe NIO.2 Watcher, qui est plus directe et plus facile à utiliser que le schéma d'interrogation des E/S d'origine.
Attention aux fautes d'orthographe
Lorsque vous copiez le code de cet article, faites attention aux fautes d'orthographe. Par exemple, l'objet StandardWatchEventKinds du listing 1 est au pluriel. Même dans la documentation Java.net, c'est mal orthographié.
Conseils
Le mécanisme de notification de NIO est plus simple à utiliser que l'ancienne méthode d'interrogation, ce qui vous incitera à ignorer l'analyse détaillée des exigences spécifiques. Lorsque vous utilisez un écouteur pour la première fois, vous devez examiner attentivement la sémantique des concepts que vous utilisez. Par exemple, il est plus important de savoir quand un changement prendra fin que de savoir quand il commencera. Ce type d'analyse doit être très prudent, en particulier pour les scénarios courants tels que le déplacement de dossiers FTP. NIO est un package très puissant, mais il comporte également quelques « pièges » subtils qui peuvent semer la confusion chez ceux qui ne le connaissent pas.
2. Sélecteurs et IO asynchrones : améliorez le multiplexage via les sélecteurs
Les nouveaux arrivants sur NIO l'associent généralement à des « entrées/sorties non bloquantes ». NIO est bien plus que de simples E/S non bloquantes, mais cette perception n'est pas entièrement fausse : les E/S de base de Java bloquent les E/S - ce qui signifie qu'elles attendent que l'opération soit terminée - cependant, les E/S non bloquantes ou asynchrones est la fonctionnalité la plus couramment utilisée de NIO, pas la totalité de NIO.
Les E/S non bloquantes de NIO sont pilotées par les événements et sont démontrées dans l'exemple d'écoute du système de fichiers dans le listing 1. Cela signifie définir un sélecteur (rappel ou écouteur) pour un canal d'E/S, puis le programme peut continuer à s'exécuter. Lorsqu'un événement se produit sur ce sélecteur - comme la réception d'une ligne d'entrée - le sélecteur "se réveille" et s'exécute. Tout cela est implémenté via un seul thread, ce qui est très différent des E/S standard de Java.
Le listing 2 montre un programme réseau multi-ports echo-er implémenté à l'aide du sélecteur de NIO. Il s'agit d'une modification d'un petit programme créé par Greg Travis en 2003 (voir la liste des ressources). Les systèmes Unix et de type Unix implémentent depuis longtemps des sélecteurs efficaces, qui constituent un bon modèle de référence pour les modèles de programmation hautes performances dans les réseaux Java.
Listing 2.Sélecteur NIO
Copiez le code comme suit :
importjava.io.*;
importjava.net.*;
importjava.nio.*;
importerjava.nio.channels.* ;
importjava.util.*;
publicclassMultiPortEcho
{
importations privées[];
privateByteBufferechoBuffer=ByteBuffer.allocate(1024);
publicMultiPortEcho(intports[])throwsIOException{
this.ports=ports;
configure_selector();
}
privatevoidconfigure_selector()throwsIOException{
//Créer un nouveau sélecteur
Selectorselector=Sélecteur.open();
// Ouvrir l'écoute de chaque port et enregistrer chacun d'eux
//aveclesélecteur
pour(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]);
}
tandis que(vrai){
intnum=sélecteur.select();
SetselectedKeys=selector.selectedKeys();
Iteratorit=selectedKeys.iterator();
while(it.hasNext()){
SelectionKeykey=(SelectionKey)it.next();
si((key.readyOps()&SelectionKey.OP_ACCEPT)
==SelectionKey.OP_ACCEPT){
//Accepterlanouvelleconnexion
ServerSocketChannelssc=(ServerSocketChannel)key.channel();
SocketChannelsc=ssc.accept();
sc.configureBlocking(false);
//Ajouterlanouvelleconnexionausélecteur
SelectionKeynewKey=sc.register(selector,SelectionKey.OP_READ);
it.remove();
System.out.println("Gotconnectionfrom"+sc);
}elseif((key.readyOps()&SelectionKey.OP_READ)
==SelectionKey.OP_READ){
//Lire les données
SocketChannelsc=(SocketChannel)key.channel();
//Échodonnées
intbytesEchoed=0;
tandis que(vrai){
echoBuffer.clear();
intnumber_of_bytes=sc.read(echoBuffer);
si (nombre_d'octets <=0) {
casser;
}
echoBuffer.flip();
sc.write(echoBuffer);
bytesEchoed+=number_of_bytes;
}
System.out.println("Echoed"+bytesEchoed+"from"+sc);
it.remove();
}
}
}
}
staticpublicvoidmain(Stringargs[])throwsException{
si(args.length<=0){
System.err.println("Usage:javaMultiPortEchoport[portport...]");
Système.exit(1);
}
intports[]=newint[args.length];
pour(inti=0;i<args.length;++i){
ports[i]=Integer.parseInt(args[i]);
}
nouveauMultiPortEcho(ports);
}
}
Compilez ce code et démarrez-le avec une commande comme javaMultiPortEcho80058006. Une fois ce programme exécuté avec succès, démarrez un simple telnet ou un autre émulateur de terminal pour vous connecter aux interfaces 8005 et 8006. Vous verrez que ce programme fait écho à tous les caractères qu'il reçoit - et ce via un thread Java.
3. Passage : promesse et réalité
Dans NIO, un canal peut représenter n'importe quel objet pouvant être lu et écrit. Son rôle est de fournir une abstraction pour les fichiers et les sockets. Les canaux NIO prennent en charge un ensemble cohérent de méthodes afin que vous n'ayez pas besoin de prêter une attention particulière aux différents objets lors du codage, qu'il s'agisse d'une sortie standard, d'une connexion réseau ou du canal utilisé. Cette fonctionnalité des canaux est héritée des flux dans les E/S de base Java. Les flux fournissent des E/S bloquantes ; les canaux prennent en charge les E/S asynchrones.
NIO est souvent recommandé pour ses hautes performances, mais plus précisément pour ses temps de réponse rapides. Dans certains scénarios, les performances de NIO sont pires que celles des E/S Java de base. Par exemple, pour une simple lecture et écriture séquentielle d'un petit fichier, les performances obtenues simplement par streaming peuvent être deux à trois fois plus rapides que l'implémentation correspondante d'encodage basé sur canal orienté événement. Dans le même temps, les canaux non multiplexes (c'est-à-dire un canal distinct par thread) sont beaucoup plus lents que plusieurs canaux enregistrant leurs sélecteurs dans le même thread.
Maintenant, lorsque vous envisagez d'utiliser des flux ou des chaînes, essayez de vous poser les questions suivantes :
De combien d’objets d’E/S avez-vous besoin pour lire et écrire ?
Les différents objets d’E/S sont-ils séquentiels ou doivent-ils tous se produire en même temps ?
Vos objets d’E/S doivent-ils persister pendant une courte période ou pendant toute la durée de vie de votre processus ?
Vos E/S sont-elles adaptées au traitement dans un seul thread ou dans plusieurs threads différents ?
La communication réseau et les E/S locales se ressemblent-elles ou ont-elles des modèles différents ?
Une telle analyse constitue une bonne pratique lorsqu’il s’agit de décider d’utiliser des flux ou des canaux. N'oubliez pas : NIO et NIO.2 ne remplacent pas les E/S de base, mais les complètent.
4. Cartographie de la mémoire - un bon acier est utilisé sur la lame
L'amélioration des performances la plus significative dans NIO est le mappage de la mémoire. Le mappage de mémoire est un service au niveau du système qui traite une section d'un fichier utilisée dans un programme comme de la mémoire.
Il existe de nombreux effets potentiels du mappage de mémoire, plus que ce que je peux fournir ici. À un niveau supérieur, cela peut faire en sorte que les performances d'E/S de l'accès aux fichiers atteignent la vitesse d'accès à la mémoire. L’accès à la mémoire est souvent plusieurs fois plus rapide que l’accès aux fichiers. Le listing 3 est un exemple simple de carte mémoire NIO.
Listing 3. Mappage mémoire dans NIO
Copiez le code comme suit :
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");
//Mappage d'un fichier dans la mémoire
MappedByteBufferout=memoryMappedFile.getChannel().map(FileChannel.MapMode.READ_WRITE,0,mem_map_size);
// Écriture dans un fichier mappé en mémoire
pour(inti=0;i<mem_map_size;i++){
out.put((octet)'A');
}
System.out.println("File'"+fn+"'isnow"+Integer.toString(mem_map_size)+"bytesfull.");
// Lecture du fichier mappé en mémoire.
pour(inti=0;i<30;i++){
System.out.print((char)out.get(i));
}
System.out.println("/nReadingfrommemory-mappedfile'"+fn+"'iscomplete.");
}
}
Dans le listing 3, cet exemple simple crée un fichier de 20 Mo example_memory_mapped_file.txt, le remplit avec le caractère A, puis lit les 30 premiers octets. Dans les applications pratiques, le mappage de mémoire permet non seulement d'améliorer la vitesse brute des E/S, mais il permet également à plusieurs lecteurs et graveurs différents de traiter la même image de fichier en même temps. Cette technologie est puissante mais aussi dangereuse, mais si elle est utilisée correctement, elle augmentera plusieurs fois votre vitesse d'E/S. Comme nous le savons tous, les opérations commerciales de Wall Street utilisent la technologie de mappage de mémoire afin d'obtenir des avantages en quelques secondes, voire millisecondes.
5.Encodage et recherche de caractères
La dernière fonctionnalité de NIO que je souhaite expliquer dans cet article est charset, un package utilisé pour convertir différents encodages de caractères. Avant NIO, Java implémentait la plupart des mêmes fonctionnalités intégrées via la méthode getByte. charset est populaire car il est plus flexible que getBytes et peut être implémenté à un niveau inférieur, ce qui entraîne de meilleures performances. Ceci est encore plus utile pour rechercher des langues autres que l'anglais qui sont sensibles à l'encodage, à l'ordre et à d'autres fonctionnalités linguistiques.
Le listing 4 montre un exemple de conversion de caractères Unicode en Java en Latin-1
Liste 4.Personnages dans NIO
Copiez le code comme suit :
Stringsome_string="CeciestunechaînequiJavanativementstockeUnicode.";
Charsetlatin1_charset=Charset.forName("ISO-8859-1");
CharsetEncodelatin1_encoder=charset.newEncoder();
ByteBufferlatin1_bbuf=latin1_encoder.encode(CharBuffer.wrap(some_string));
Notez que les jeux de caractères et les canaux sont conçus pour être utilisés ensemble, afin que le programme puisse s'exécuter normalement lorsque le mappage de mémoire, les E/S asynchrones et la conversion de codage sont coordonnés.
Résumé : Bien sûr, il y a plus à savoir
Le but de cet article est de familiariser les développeurs Java avec certaines des fonctionnalités les plus importantes (et utiles) de NIO et NIO.2. Vous pouvez utiliser les bases établies par ces exemples pour comprendre d'autres méthodes de NIO ; par exemple, les connaissances que vous acquérez sur les canaux peuvent vous aider à comprendre le traitement des liens symboliques dans le système de fichiers dans le chemin de NIO. Vous pouvez également vous référer à la liste de ressources que j'ai donnée plus tard, qui fournit des documents pour une étude approfondie de la nouvelle API d'E/S de Java.