Java NIO (Nueva entrada/salida), el nuevo paquete API de entrada/salida, se introdujo en J2SE 1.4 en 2002. El objetivo de Java NIO es mejorar el rendimiento de tareas intensivas de E/S en la plataforma Java. Diez años después, muchos desarrolladores de Java todavía no saben cómo hacer un uso completo de NIO, y aún menos personas saben que la API de entrada/salida actualizada (NIO.2) se introdujo en Java SE 7. La mayor contribución de NIO y NIO.2 a la plataforma Java es mejorar el rendimiento de un componente central en el desarrollo de aplicaciones Java: el procesamiento de entrada/salida. Sin embargo, ninguno de los paquetes es muy útil y no son adecuados para todos los escenarios. Si se usan correctamente, Java NIO y NIO.2 pueden reducir en gran medida el tiempo dedicado a algunas operaciones de E/S comunes. Este es el superpoder de NIO y NIO.2, y en este artículo te mostraré 5 formas sencillas de utilizarlos.
Cambiar notificaciones (porque cada evento requiere un oyente)
Selectores y IO asíncrono: Mejorando la multiplexación a través de selectores
Canal - Promesa y Realidad
Mapeo de memoria: se utiliza buen acero en la hoja.
Codificación y búsqueda de caracteres.
Antecedentes de NIO
¿Por qué un paquete de mejora que existe desde hace 10 años es un nuevo paquete de E/S para Java? La razón es que para la mayoría de los programadores de Java, las operaciones básicas de E/S son suficientes. En el trabajo diario, la mayoría de los desarrolladores de Java no necesitan aprender NIO. Yendo un paso más allá, NIO es más que un simple paquete de mejora del rendimiento. En cambio, es una colección de diferentes funciones relacionadas con Java I/O. NIO logra una mejora del rendimiento al acercar el rendimiento de las aplicaciones Java a la esencia, lo que significa que las API de NIO y NIO.2 exponen la entrada a operaciones del sistema de bajo nivel. El precio de NIO es que, si bien proporciona capacidades de control de E/S más potentes, también requiere que lo usemos y practiquemos con más cuidado que la programación de E/S básica. Otra característica de NIO es su enfoque en la expresividad de la aplicación, que veremos en los siguientes ejercicios.
Empiece a aprender NIO y NIO.2
Hay muchos materiales de referencia para NIO: algunos enlaces seleccionados en los materiales de referencia. Para aprender NIO y NIO.2, la documentación de Java 2 SDK Standard Edition (SE) y la documentación de Java SE 7 son indispensables. Para utilizar el código de este artículo, debe utilizar JDK 7 o superior.
Para muchos desarrolladores, la primera vez que se encuentran con NIO puede ser cuando mantienen aplicaciones: una aplicación que funciona es cada vez más lenta, por lo que algunas personas sugieren usar NIO para mejorar la velocidad de respuesta. NIO es excelente cuando se trata de mejorar el rendimiento de las aplicaciones, pero los resultados específicos dependen del sistema subyacente (tenga en cuenta que NIO depende de la plataforma). Si es la primera vez que usa NIO, debe pesarlo con cuidado. Descubrirá que la capacidad de NIO para mejorar el rendimiento depende no solo del sistema operativo, sino también de la JVM que esté utilizando, el contexto virtual del host, las características del almacenamiento masivo e incluso los datos. Por lo tanto, el trabajo de medición del desempeño es relativamente difícil de realizar. Especialmente cuando su sistema tiene un entorno de implementación portátil, debe prestar especial atención.
Después de comprender el contenido anterior, no tenemos preocupaciones. Ahora experimentemos las cinco funciones importantes de NIO y NIO.2.
1. Notificación de cambio (porque cada evento requiere un oyente)
Una preocupación común entre los desarrolladores interesados en NIO y NIO.2 es el rendimiento de las aplicaciones Java. En mi experiencia, el notificador de cambio de archivos en NIO.2 es la característica más interesante (y subestimada) de la nueva API de entrada/salida.
Muchas aplicaciones de nivel empresarial requieren un procesamiento especial en las siguientes situaciones:
Cuando se carga un archivo en una carpeta FTP
Cuando se modifica una definición en una configuración
Cuando se carga un borrador de documento
Cuando ocurren otros eventos del sistema de archivos
Todos estos son ejemplos de notificaciones de cambios o respuestas de cambios. En versiones anteriores de Java (y otros lenguajes), el sondeo era la mejor manera de detectar estos eventos de cambio. El sondeo es un tipo especial de bucle infinito: verifique el sistema de archivos u otros objetos y compárelo con el estado anterior. Si no hay cambios, continúe verificando después de un intervalo de aproximadamente unos cientos de milisegundos o 10 segundos. Esto continúa en un bucle infinito.
NIO.2 proporciona una mejor manera de realizar la detección de cambios. El Listado 1 es un ejemplo sencillo.
Listado 1. Cambiar mecanismo de notificación en NIO.2
Copie el código de código de la siguiente manera:
importar java.nio.file.attribute.*;
importarjava.io.*;
importarjava.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;
observador de clase pública{
publicstaticvoidmain(String[]argumentos){
Paththis_dir=Paths.get(".");
System.out.println("Ahora observando el directorio actual...");
intentar{
WatchServicewatcher=this_dir.getFileSystem().newWatchService();
this_dir.register(observador,StandardWatchEventKinds.ENTRY_CREATE);
WatchKeywatckKey=watcher.take();
Lista<WatchEvent<<64;>>events=watckKey.pollEvents();
para(WatchEventevent:eventos){
System.out.println("Alguien acaba de crear el archivo'"+event.context().toString()+"'.");
}
}catch(Excepción){
System.out.println("Error:"+e.toString());
}
}
}
Compile este código y ejecútelo desde la línea de comando. En el mismo directorio, cree un nuevo archivo, por ejemplo, ejecute el comando touchexample o copyWatcher.classexample. Verá el siguiente mensaje de notificación de cambio:
Alguien simplemente crea el campo 'ejemplo1'.
Este sencillo ejemplo muestra cómo empezar a utilizar la funcionalidad JavaNIO. Al mismo tiempo, también presenta la clase NIO.2 Watcher, que es más directa y más fácil de usar que el esquema de sondeo en la E/S original.
Cuidado con los errores ortográficos
Cuando copie el código de este artículo, tenga cuidado con los errores ortográficos. Por ejemplo, el objeto StandardWatchEventKinds del Listado 1 está en plural. Incluso en la documentación de Java.net está mal escrito.
Consejos
El mecanismo de notificación en NIO es más sencillo de usar que el antiguo método de sondeo, lo que le inducirá a ignorar el análisis detallado de requisitos específicos. Cuando utiliza un oyente por primera vez, debe considerar cuidadosamente la semántica de los conceptos que está utilizando. Por ejemplo, saber cuándo terminará un cambio es más importante que saber cuándo comienza. Este tipo de análisis debe ser muy cuidadoso, especialmente en escenarios comunes como mover carpetas FTP. NIO es un paquete muy poderoso, pero también tiene algunos "errores" sutiles que pueden causar confusión a quienes no están familiarizados con él.
2. Selectores y IO asíncrona: mejorar la multiplexación a través de selectores
Los recién llegados a NIO generalmente lo asocian con "entrada/salida sin bloqueo". NIO es más que simplemente E/S sin bloqueo, pero esta percepción no es del todo errónea: la E/S básica de Java bloquea E/S, lo que significa que espera hasta que se complete la operación; sin embargo, E/S sin bloqueo o asincrónicas es la característica más utilizada de NIO, no toda NIO.
La E/S sin bloqueo de NIO está controlada por eventos y se demuestra en el ejemplo de escucha del sistema de archivos en el Listado 1. Esto significa definir un selector (devolución de llamada o escucha) para un canal de E/S, y luego el programa puede continuar ejecutándose. Cuando ocurre un evento en este selector, como recibir una línea de entrada, el selector "se activa" y se ejecuta. Todo esto se implementa a través de un único subproceso, que es significativamente diferente de la E/S estándar de Java.
El Listado 2 muestra un programa de red multipuerto echo-er implementado usando el selector de NIO. Este es un pequeño programa creado por Greg Travis en 2003 (consulte la lista de recursos). Unix y los sistemas similares a Unix han implementado desde hace mucho tiempo selectores eficientes, que son un buen modelo de referencia para modelos de programación de alto rendimiento en redes Java.
Listado 2.Selector NIO
Copie el código de código de la siguiente manera:
importarjava.io.*;
importarjava.net.*;
importarjava.nio.*;
importjava.nio.channels.*;
importarjava.util.*;
clase públicaMultiPortEcho
{
puertosprivados[];
privateByteBufferechoBuffer=ByteBuffer.allocate(1024);
publicMultiPortEcho(intports[])throwsIOException{
this.ports=puertos;
configurar_selector();
}
privatevoidconfigure_selector()throwsIOException{
//Createanewselector
Selectorselector=Selector.open();
//Abrelistencadapuerto,yregistrecadauno
//conelselector
for(inti=0;i<puertos.longitud;++i){
ServerSocketChannelssc=ServerSocketChannel.open();
ssc.configureBlocking (falso);
ServerSockets=ssc.socket();
InetSocketAddressaddress=newInetSocketAddress(puertos[i]);
ss.bind(dirección);
SelectionKeykey=ssc.register(selector,SelectionKey.OP_ACCEPT);
System.out.println("Indo a escuchar"+puertos[i]);
}
mientras (verdadero) {
intnum=selector.select();
SetselectedKeys=selector.selectedKeys();
Iteratorit=claves seleccionadas.iterator();
mientras(es.tieneSiguiente()){
SelectionKeykey=(SelectionKey)it.next();
si((key.readyOps()&SelectionKey.OP_ACCEPT)
==Clave de selección.OP_ACCEPT){
//aceptamos la nueva conexión
ServerSocketChannelssc=(ServerSocketChannel)key.channel();
SocketChannelsc=ssc.accept();
sc.configureBlocking (falso);
//Agrega la nueva conexión al selector
SelectionKeynewKey=sc.register(selector,SelectionKey.OP_READ);
es.remove();
System.out.println("Obtuve conexión de"+sc);
}elseif((key.readyOps()&SelectionKey.OP_READ)
==Clave de selección.OP_READ){
//Leer los datos
SocketChannelsc=(SocketChannel)key.channel();
//Echodatos
intbytesEchoed=0;
mientras (verdadero) {
echoBuffer.clear();
intnumber_of_bytes=sc.read(echoBuffer);
si(número_de_bytes<=0){
romper;
}
echoBuffer.flip();
sc.write(echoBuffer);
bytesEchoed+=número_de_bytes;
}
System.out.println("Echoed"+bytesEchoed+"from"+sc);
es.remove();
}
}
}
}
staticpublicvoidmain(Stringargs[])throwsException{
si(args.longitud<=0){
System.err.println("Uso:javaMultiPortEchoport[puerto...]");
Sistema.salir(1);
}
intports[]=newint[args.length];
for(inti=0;i<args.length;++i){
puertos[i]=Integer.parseInt(args[i]);
}
newMultiPortEcho(puertos);
}
}
Compile este código e inícielo con un comando similar a javaMultiPortEcho80058006. Una vez que este programa se ejecute correctamente, inicie un telnet simple u otro emulador de terminal para conectarse a las interfaces 8005 y 8006. Verás que este programa repite todos los caracteres que recibe, y lo hace a través de un hilo de Java.
3. Pasaje: Promesa y Realidad
En NIO, un canal puede representar cualquier objeto que se pueda leer y escribir. Su función es proporcionar abstracción para archivos y sockets. Los canales NIO admiten un conjunto coherente de métodos, por lo que no es necesario prestar especial atención a diferentes objetos al codificar, ya sea una salida estándar, una conexión de red o el canal en uso. Esta característica de los canales se hereda de los flujos en la E/S básica de Java. Las transmisiones proporcionan bloqueo de E/S; los canales admiten E/S asíncronas.
A menudo se recomienda NIO por su alto rendimiento, pero más precisamente por sus rápidos tiempos de respuesta. En algunos escenarios, el rendimiento de NIO es peor que el de E/S básica de Java. Por ejemplo, para una simple lectura y escritura secuencial de un archivo pequeño, el rendimiento logrado simplemente mediante la transmisión puede ser dos o tres veces más rápido que la correspondiente implementación de codificación basada en canales orientada a eventos. Al mismo tiempo, los canales no multiplexados, es decir, un canal separado por subproceso, son mucho más lentos que los canales múltiples que registran sus selectores en el mismo subproceso.
Ahora, cuando estés considerando si utilizar transmisiones o canales, intenta hacerte las siguientes preguntas:
¿Cuántos objetos de E/S necesitas para leer y escribir?
¿Los diferentes objetos de E/S son secuenciales o es necesario que ocurran todos al mismo tiempo?
¿Es necesario que sus objetos de E/S persistan durante un corto período de tiempo o durante toda la vida útil de su proceso?
¿Su E/S es adecuada para procesar en un solo subproceso o en varios subprocesos diferentes?
¿La comunicación de red y la E/S local tienen el mismo aspecto o tienen patrones diferentes?
Este análisis es una práctica recomendada a la hora de decidir si utilizar transmisiones o canales. Recuerde: NIO y NIO.2 no reemplazan las E/S básicas, sino que las complementan.
4. Mapeo de memoria: se utiliza buen acero en la hoja.
La mejora de rendimiento más significativa en NIO es el mapeo de memoria. El mapeo de memoria es un servicio a nivel de sistema que trata una sección de un archivo utilizado en un programa como memoria.
Hay muchos efectos potenciales del mapeo de memoria, más de los que puedo proporcionar aquí. En un nivel superior, puede hacer que el rendimiento de E/S del acceso a archivos alcance la velocidad del acceso a la memoria. El acceso a la memoria suele ser mucho más rápido que el acceso a los archivos. El Listado 3 es un ejemplo simple de un mapa de memoria NIO.
Listado 3. Mapeo de memoria en NIO
Copie el código de código de la siguiente manera:
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");
//Asignación de un archivo a la memoria
MappedByteBufferout=memoryMappedFile.getChannel().map(FileChannel.MapMode.READ_WRITE,0,mem_map_size);
//Escribiendo en un archivo asignado a la memoria
para(inti=0;i<mem_map_size;i++){
salida.put((byte)'A');
}
System.out.println("File'"+fn+"'isnow"+Integer.toString(mem_map_size)+"bytesfull.");
// Leer desde el archivo mapeado en memoria.
para(inti=0;i<30;i++){
System.out.print((char)out.get(i));
}
System.out.println("/nLa lectura del archivo mapeado en memoria'"+fn+"'está completa.");
}
}
En el Listado 3, este ejemplo simple crea un archivo de 20 M example_memory_mapped_file.txt, lo llena con el carácter A y luego lee los primeros 30 bytes. En aplicaciones prácticas, el mapeo de memoria no solo es bueno para mejorar la velocidad bruta de E/S, sino que también permite que múltiples lectores y escritores diferentes procesen la misma imagen de archivo al mismo tiempo. Esta tecnología es poderosa pero también peligrosa, pero si se usa correctamente, aumentará su velocidad IO varias veces. Como todos sabemos, las operaciones comerciales de Wall Street utilizan tecnología de mapeo de memoria para obtener ventajas en segundos o incluso milisegundos.
5.Codificación y búsqueda de caracteres
La última característica de NIO que quiero explicar en este artículo es el juego de caracteres, un paquete utilizado para convertir diferentes codificaciones de caracteres. Antes de NIO, Java implementaba la mayor parte de la misma funcionalidad integrada a través del método getByte. charset es popular porque es más flexible que getBytes y puede implementarse en un nivel inferior, lo que resulta en un mejor rendimiento. Esto es aún más valioso para buscar idiomas distintos del inglés que son sensibles a la codificación, el orden y otras características del idioma.
El Listado 4 muestra un ejemplo de conversión de caracteres Unicode en Java a Latin-1
Lista 4.Caracteres en NIO
Copie el código de código de la siguiente manera:
Stringsome_string="Esta es una cadena que Java almacena Unicode.";
Charsetlatin1_charset=Charset.forName("ISO-8859-1");
CharsetEncodelatin1_encoder=charset.newEncoder();
ByteBufferlatin1_bbuf=latin1_encoder.encode(CharBuffer.wrap(some_string));
Tenga en cuenta que los conjuntos de caracteres y los canales están diseñados para usarse juntos, de modo que el programa pueda ejecutarse normalmente cuando se coordinan el mapeo de memoria, la E/S asíncrona y la conversión de codificación.
Resumen: Por supuesto que hay más que saber
El propósito de este artículo es familiarizar a los desarrolladores de Java con algunas de las características más importantes (y útiles) de NIO y NIO.2. Puede utilizar la base establecida por estos ejemplos para comprender algunos otros métodos de NIO, por ejemplo, el conocimiento que aprenda sobre los canales puede ayudarlo a comprender el procesamiento de enlaces simbólicos en el sistema de archivos en la ruta de NIO; También puede consultar la lista de recursos que proporcioné más adelante, que proporciona algunos documentos para un estudio en profundidad de la nueva API de E/S de Java.