Cinque modi per massimizzare Java NIO e NIO.2

Java NIO, il nuovo pacchetto API di input / output, è stato introdotto con J2SE 1.4 nel 2002. Lo scopo di Java NIO era migliorare la programmazione delle attività ad alta intensità di I / O sulla piattaforma Java. Un decennio dopo, molti programmatori Java non sanno ancora come utilizzare al meglio NIO e ancora meno sono consapevoli che Java SE 7 ha introdotto More New Input / Output APIs (NIO.2). In questo tutorial troverai cinque semplici esempi che dimostrano i vantaggi dei pacchetti NIO e NIO.2 in scenari di programmazione Java comuni.

Il contributo principale di NIO e NIO.2 alla piattaforma Java è il miglioramento delle prestazioni in una delle aree principali dello sviluppo di applicazioni Java: elaborazione di input / output. Nessuno dei due pacchetti è particolarmente facile da lavorare, né sono necessarie le nuove API di input / output per ogni scenario di I / O Java. Utilizzati correttamente, tuttavia, Java NIO e NIO.2 possono ridurre drasticamente il tempo necessario per alcune comuni operazioni di I / O. Questo è il superpotere di NIO e NIO.2 e questo articolo presenta cinque modi relativamente semplici per sfruttarlo:

  1. Notificatori di cambiamento (perché tutti hanno bisogno di un ascoltatore)
  2. I selettori aiutano il multiplex
  3. Canali: promessa e realtà
  4. Mappatura della memoria: dove conta
  5. Codifica dei caratteri e ricerca

Il contesto NIO

Com'è che un miglioramento di 10 anni è ancora il nuovo pacchetto Input / Output per Java? Il motivo è che per molti programmatori Java che lavorano le operazioni di I / O Java di base sono più che adeguate. La maggior parte degli sviluppatori Java non deve imparare a usare NIO per il nostro lavoro quotidiano. Inoltre, NIO non è solo un pacchetto di prestazioni. Invece, è una raccolta eterogenea di funzionalità relative all'I / O Java. NIO aumenta le prestazioni delle applicazioni Java avvicinandosi "al metallo" di un programma Java, il che significa che le API NIO e NIO.2 espongono punti di ingresso del sistema operativo (OS) di livello inferiore. Il compromesso di NIO è che ci dà simultaneamente un maggiore controllo sull'I / O e richiede che esercitiamo più attenzione di quanto faremmo con la programmazione I / O di base. Un altro aspetto di NIO è la sua attenzione all'espressività dell'applicazione, con cui giocheremo in alcuni degli esercizi che seguono.

Iniziando con NIO e NIO.2

Sono disponibili molti buoni riferimenti per NIO: vedere Risorse per alcuni collegamenti selezionati. Per iniziare con NIO e NIO.2, sono indispensabili la documentazione Java 2 SDK Standard Edition (SE) e la documentazione Java SE 7. Per eseguire gli esempi in questo articolo è necessario lavorare con JDK 7 o superiore.

Per molti sviluppatori il primo incontro con NIO potrebbe accadere durante i lavori di manutenzione: un'applicazione ha funzionalità corrette ma è lenta a rispondere, quindi qualcuno suggerisce di utilizzare NIO per accelerarla. NIO brilla quando viene utilizzato per aumentare le prestazioni di elaborazione, ma i suoi risultati saranno strettamente legati alla piattaforma sottostante. (Nota che NIO dipende dalla piattaforma.) Se stai usando NIO per la prima volta, ti ripagherà di misurare con attenzione. Potresti scoprire che la capacità di NIO di accelerare le prestazioni delle applicazioni dipende non solo dal sistema operativo, ma dalla JVM specifica, dal contesto di virtualizzazione dell'host, dalle caratteristiche dell'archiviazione di massa e persino dai dati. Tuttavia, la misurazione può essere difficile da generalizzare. Tienilo a mente in particolare se una distribuzione mobile è tra i tuoi obiettivi.

E ora, senza ulteriori indugi, esploriamo cinque importanti strutture di NIO e NIO.

1. Cambia notificanti (perché tutti hanno bisogno di un ascoltatore)

Le prestazioni delle applicazioni Java sono l'attrazione comune per gli sviluppatori interessati a NIO o NIO. Nella mia esperienza, tuttavia, il notificatore di modifiche ai file di NIO.2 è la caratteristica più convincente (se sottovalutata) delle nuove API di input / output.

Molte applicazioni di classe enterprise devono eseguire un'azione specifica quando:

  • Un file viene caricato in una cartella FTP
  • Viene modificata una definizione di configurazione
  • Viene aggiornata una bozza di documento
  • Si verifica un altro evento del file system

Questi sono tutti esempi di notifica di modifica o risposta di modifica. Nelle prime versioni di Java (e di altri linguaggi), il polling era in genere il modo migliore per rilevare gli eventi di modifica. Il polling è un particolare tipo di ciclo infinito: controlla il file system o un altro oggetto, confrontalo con il suo ultimo stato noto e, se non ci sono modifiche, ricontrolla dopo un breve intervallo, come cento millisecondi o dieci secondi . Continua il ciclo indefinitamente.

NIO.2 ci offre un modo migliore per esprimere il rilevamento delle modifiche. Il listato 1 è un semplice esempio.

Listato 1. Notifica di modifica in NIO.2

import java.nio.file.attribute.*; import java.io.*; import java.util.*; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchEvent; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.util.List; public class Watcher { public static void main(String[] args) { Path this_dir = Paths.get("."); System.out.println("Now watching the current directory ..."); try { WatchService watcher = this_dir.getFileSystem().newWatchService(); this_dir.register(watcher, StandardWatchEventKinds.ENTRY_CREATE); WatchKey watckKey = watcher.take(); List
    
      events = watckKey.pollEvents(); for (WatchEvent event : events) { System.out.println("Someone just created the file '" + event.context().toString() + "'."); } } catch (Exception e) { System.out.println("Error: " + e.toString()); } } }
    

Compila questa sorgente, quindi avvia l'eseguibile della riga di comando. Nella stessa directory, crea un nuovo file; potresti, ad esempio touch example1, o anche copy Watcher.class example1. Dovresti vedere il seguente messaggio di notifica delle modifiche:

Someone just create the file 'example1'.

Questo semplice esempio illustra come iniziare ad accedere alle strutture linguistiche di NIO in Java. Introduce inoltre la Watcherclasse NIO.2 , che è notevolmente più semplice e facile da usare per la notifica delle modifiche rispetto alla tradizionale soluzione I / O basata sul polling.

Attenzione agli errori di battitura!

Fai attenzione quando copi la fonte da questo articolo. Nota, ad esempio, che l' StandardWatchEventKindsoggetto nel listato 1 è scritto al plurale. Persino la documentazione di Java.net lo mancava!

Mancia

I notificatori di NIO sono molto più facili da usare rispetto ai vecchi cicli di polling che si è tentati di trascurare l'analisi dei requisiti. Ma dovresti riflettere su queste semantiche la prima volta che usi un listener. Sapere quando la modifica di un file finisce è più utile che sapere quando inizia, per esempio. Questo tipo di analisi richiede una certa attenzione, specialmente in un caso comune come la cartella di rilascio FTP. NIO è un potente pacchetto con alcuni sottili "trucchi"; può punire un visitatore occasionale.

2. Selettori e I / O asincroni: i selettori aiutano il multiplex

I nuovi arrivati ​​a NIO a volte lo associano a "input / output non bloccanti". NIO è più di un I / O non bloccante, ma l'errore ha senso: l'I / O di base in Java è bloccante , il che significa che attende fino a quando non può completare un'operazione, mentre l'I / O non bloccante o asincrono è una delle strutture NIO più utilizzate.

L'I / O non bloccante di NIO è basato sugli eventi , come dimostrato dal listener del file system nel Listato 1. Ciò significa che un selettore (o callback o listener) è definito per un canale I / O, quindi l'elaborazione continua. Quando si verifica un evento sul selettore, ad esempio quando arriva una riga di input, il selettore si "sveglia" ed esegue. Tutto ciò si ottiene all'interno di un singolo thread , che è in netto contrasto con il tipico I / O Java.

Listing 2 demonstrates the use of NIO selectors in a multi-port networking echo-er, a program slightly modified from one created by Greg Travis in 2003 (see Resources). Unix and Unix-like operating systems have long had efficient implementations of selectors, so this sort of networking program is a model of good performance for a Java-coded networking program.

Listing 2. NIO selectors

import java.io.*; import java.net.*; import java.nio.*; import java.nio.channels.*; import java.util.*; public class MultiPortEcho { private int ports[]; private ByteBuffer echoBuffer = ByteBuffer.allocate( 1024 ); public MultiPortEcho( int ports[] ) throws IOException { this.ports = ports; configure_selector(); } private void configure_selector() throws IOException { // Create a new selector Selector selector = Selector.open(); // Open a listener on each port, and register each one // with the selector for (int i=0; i
    

Compile this source, then launch it from the command-line with an invocation such as java MultiPortEcho 8005 8006. Once the MultiPortEchoer is running, start up a simple telnet or other terminal emulator running against ports 8005 and 8006. You will see that the program echoes back characters it receives -- and does it in a single Java thread!

More NIO on JavaWorld

See the following JavaWorld articles for more background on the java.nio package APIs.

  • "Master Merlin's new I/O classes" (Michael T. Nygard, JavaWorld, September 2001)
  • "Use select for high-speed networking" (Greg Travis, JavaWorld, April 2003)

3. Channels: Promise and reality

In NIO, a channel can be any object that reads or writes. Its job is to abstract files and sockets. NIO channels support a consistent collection of methods, so it's possible to program without having special cases depending on whether stdout, a network connection, or some other channel is actually in use. Channels share this characteristic of the streams of Java's basic I/O. Streams provide blocking I/O; channels support asynchronous I/O.

While NIO is often promoted for its performance advantages, it's more precise to say it is highly responsive. In some cases NIO actually performs worse than basic Java I/O. For simple sequential reads and writes of small files, for instance, a straightforward streams implementation might be two or three times faster than the corresponding event-oriented channel-based coding. Also, non-multiplexed channels -- that is, channels in separate threads -- can be much slower than channels that register their selectors in a single thread.

The next time you need to define a programming problem in terms of dimensions pertinent to streams or channels, try asking the following questions:

  • How many I/O objects must you read and write?
  • Is there a natural sequence between different I/O objects or might they all need to happen simultaneously?
  • Do your I/O objects only last for a short interval or are they likely to persist during the lifetime of your process?
  • Is it more natural to do your I/O in a single thread or several distinct ones?
  • Is network traffic likely to look the same as local I/O or do the two have different patterns?

This sort of analysis is good practice for deciding when to use streams or channels. Remember: NIO and NIO.2 don't replace basic I/O; they just supplement it.

4. Memory mapping -- where it counts

The most consistently dramatic performance improvement in the use of NIO involves memory mapping. Memory mapping is an OS-level service that makes segments of a file appear for programming purposes like areas of memory.