Java NIO (New Input/Output) - the new input/output API package - was introduced in J2SE 1.4 in 2002. The goal of Java NIO is to improve the performance of I/O-intensive tasks on the Java platform. Ten years later, many Java developers still don't know how to make full use of NIO, and even fewer people know that the updated input/output API (NIO.2) was introduced in Java SE 7. The biggest contribution of NIO and NIO.2 to the Java platform is to improve the performance of a core component in Java application development: input/output processing. However, neither package is very useful, and they are not suitable for all scenarios. If used correctly, Java NIO and NIO.2 can greatly reduce the time spent on some common I/O operations. This is the super power of NIO and NIO.2, and in this article I will show you 5 simple ways to use them.
Change notifications (because every event requires a listener)
Selectors and asynchronous IO: Improving multiplexing through selectors
Channel - Promise and Reality
Memory mapping - good steel is used on the blade
Character encoding and search
NIO’s background
Why is an enhancement package that has been around for 10 years a new I/O package for Java? The reason is that for most Java programmers, basic I/O operations are sufficient. In daily work, most Java developers do not need to learn NIO. Taking it a step further, NIO is more than just a performance improvement package. Instead, it is a collection of different functions related to Java I/O. NIO achieves performance improvement by making the performance of Java applications "closer to the essence", which means that the APIs of NIO and NIO.2 expose the entrance to low-level system operations. The price of NIO is that while it provides more powerful I/O control capabilities, it also requires us to use and practice more carefully than basic I/O programming. Another feature of NIO is its focus on application expressiveness, which we will see in the following exercises.
Start learning NIO and NIO.2
There are many reference materials for NIO - some selected links in the reference materials. To learn NIO and NIO.2, Java 2 SDK Standard Edition (SE) documentation and Java SE 7 documentation are indispensable. To use the code in this article, you need to use JDK 7 or higher.
For many developers, the first time they encounter NIO may be when maintaining applications: a functioning application is getting slower and slower, so some people suggest using NIO to improve response speed. NIO is more outstanding when it comes to improving application performance, but the specific results depend on the underlying system. (Note that NIO is platform-dependent). If this is your first time using NIO, you need to weigh it carefully. You will find that NIO's ability to improve performance depends not only on the OS, but also on the JVM you are using, the virtual context of the host, the characteristics of mass storage, and even the data. Therefore, the work of performance measurement is relatively difficult to do. Especially when your system has a portable deployment environment, you need to pay special attention.
After understanding the above content, we have no worries. Now let’s experience the five important functions of NIO and NIO.2.
1. Change notification (because each event requires a listener)
A common concern among developers interested in NIO and NIO.2 is the performance of Java applications. In my experience, the file change notifier in NIO.2 is the most interesting (and underrated) feature of the new input/output API.
Many enterprise-level applications require special processing in the following situations:
When a file is uploaded to an FTP folder
When a definition in a configuration is modified
When a draft document is uploaded
When other file system events occur
These are all examples of change notifications or change responses. In earlier versions of Java (and other languages), polling was the best way to detect these change events. Polling is a special kind of infinite loop: check the file system or other objects, and compare it with the previous state. If there is no change, continue checking after an interval of about a few hundred milliseconds or 10 seconds. This goes on in an infinite loop.
NIO.2 provides a better way to perform change detection. Listing 1 is a simple example.
Listing 1. Change notification mechanism in NIO.2
Copy the code code as follows:
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...");
try{
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(Exceptione){
System.out.println("Error:"+e.toString());
}
}
}
Compile this code and execute it from the command line. In the same directory, create a new file, for example, run the touchexample or copyWatcher.classexample command. You will see the following change notification message:
Someonejustcreatethefiel'example1′.
This simple example shows how to start using JavaNIO functionality. At the same time, it also introduces the NIO.2 Watcher class, which is more direct and easier to use than the polling scheme in the original I/O.
Watch out for spelling errors
When you copy the code from this article, watch out for spelling errors. For example, the StandardWatchEventKinds object in Listing 1 is in the plural form. Even in the Java.net documentation it is spelled wrong.
Tips
The notification mechanism in NIO is simpler to use than the old polling method, which will induce you to ignore detailed analysis of specific requirements. When you first use a listener, you need to carefully consider the semantics of the concepts you are using. For example, knowing when a change will end is more important than knowing when it begins. This kind of analysis needs to be very careful, especially for common scenarios like moving FTP folders. NIO is a very powerful package, but it also has some subtle "gotchas" that can cause confusion for those unfamiliar with it.
2. Selectors and asynchronous IO: Improve multiplexing through selectors
Newcomers to NIO generally associate it with "non-blocking input/output". NIO is more than just non-blocking I/O, but this perception is not entirely wrong: Java's basic I/O is blocking I/O - meaning it waits until the operation is completed - however, non- Blocking or asynchronous I/O is the most commonly used feature of NIO, not all of NIO.
NIO's non-blocking I/O is event-driven and is demonstrated in the file system listening example in Listing 1. This means defining a selector (callback or listener) for an I/O channel, and then the program can continue running. When an event occurs on this selector - such as receiving a line of input - the selector "wakes up" and executes. All of this is implemented through a single thread, which is significantly different from Java's standard I/O.
Listing 2 shows a multi-port network program echo-er implemented using NIO's selector. This is a modification of a small program created by Greg Travis in 2003 (refer to the resource list). Unix and Unix-like systems have long implemented efficient selectors, which are a good reference model for high-performance programming models in Java networks.
Listing 2.NIO selector
Copy the code code as follows:
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{
//Createanewselector
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(address);
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){
//Acceptthenewconnection
ServerSocketChannelssc=(ServerSocketChannel)key.channel();
SocketChannelsc=ssc.accept();
sc.configureBlocking(false);
//Addthenewconnectiontotheselector
SelectionKeynewKey=sc.register(selector,SelectionKey.OP_READ);
it.remove();
System.out.println("Gotconnectionfrom"+sc);
}elseif((key.readyOps()&SelectionKey.OP_READ)
==SelectionKey.OP_READ){
//Readthedata
SocketChannelsc=(SocketChannel)key.channel();
//Echodata
intbytesEchoed=0;
while(true){
echoBuffer.clear();
intnumber_of_bytes=sc.read(echoBuffer);
if(number_of_bytes<=0){
break;
}
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);
}
}
Compile this code and start it with a command like javaMultiPortEcho80058006. Once this program runs successfully, start a simple telnet or other terminal emulator to connect to the 8005 and 8006 interfaces. You'll see that this program echoes all the characters it receives - and it does so through a Java thread.
3. Passage: Promise and Reality
In NIO, a channel can represent any object that can be read and written. Its role is to provide abstraction for files and sockets. NIO channels support a consistent set of methods so that you don't need to pay special attention to different objects when encoding, whether it is standard output, a network connection, or the channel in use. This feature of channels is inherited from streams in Java basic I/O. Streams provide blocking IO; channels support asynchronous I/O.
NIO is often recommended for its high performance, but more accurately for its fast response times. In some scenarios, NIO's performance is worse than basic Java I/O. For example, for a simple sequential read and write of a small file, the performance achieved simply by streaming may be two to three times faster than the corresponding event-oriented channel-based encoding implementation. At the same time, non-multiplex channels—that is, a separate channel per thread—are much slower than multiple channels registering their selectors in the same thread.
Now when you are considering whether to use streams or channels, try asking yourself the following questions:
How many I/O objects do you need to read and write?
Are different I/O objects sequential, or do they all need to happen at the same time?
Do your I/O objects need to persist for a short period of time or for the entire lifetime of your process?
Is your I/O suitable for processing in a single thread or in several different threads?
Do network communication and local I/O look the same, or do they have different patterns?
Such analysis is a best practice when deciding whether to use streams or channels. Remember: NIO and NIO.2 are not replacements for basic I/O, but a complement to it.
4. Memory mapping - good steel is used on the blade
The most significant performance improvement in NIO is memory mapping. Memory mapping is a system-level service that treats a section of a file used in a program as memory.
There are many potential effects of memory mapping, more than I can provide here. At a higher level, it can make the I/O performance of file access reach the speed of memory access. Memory access is often orders of magnitude faster than file access. Listing 3 is a simple example of a NIO memory map.
Listing 3. Memory mapping in NIO
Copy the code code as follows:
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);
//WritingintoMemoryMappedFile
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, this simple example creates a 20M file example_memory_mapped_file.txt, fills it with the character A, and then reads the first 30 bytes. In practical applications, memory mapping is not only good at improving the raw speed of I/O, but it also allows multiple different readers and writers to process the same file image at the same time. This technology is powerful but also dangerous, but if used correctly, it will increase your IO speed several times. As we all know, Wall Street trading operations use memory mapping technology in order to gain advantages in seconds or even milliseconds.
5.Character encoding and search
The last feature of NIO I want to explain in this article is charset, a package used to convert different character encodings. Before NIO, Java implemented most of the same functionality built-in through the getByte method. charset is popular because it is more flexible than getBytes and can be implemented at a lower level, which results in better performance. This is even more valuable for searching non-English languages that are sensitive to encoding, ordering, and other language features.
Listing 4 shows an example of converting Unicode characters in Java to Latin-1
List 4.Characters in NIO
Copy the code code as follows:
Stringsome_string="ThisisastringthatJavanativelystoresUnicode.";
Charsetlatin1_charset=Charset.forName("ISO-8859-1");
CharsetEncodelatin1_encoder=charset.newEncoder();
ByteBufferlatin1_bbuf=latin1_encoder.encode(CharBuffer.wrap(some_string));
Note that Charsets and channels are designed to be used together, so that the program can run normally when memory mapping, asynchronous I/O, and encoding conversion are coordinated.
Summary: Of course there is more to know
The purpose of this article is to familiarize Java developers with some of the most important (and useful) features in NIO and NIO.2. You can use the foundation established by these examples to understand some other methods of NIO; for example, the knowledge you learn about channels can help you understand the processing of symbolic links in the file system in NIO's Path. You can also refer to the resource list I gave later, which provides some documents for in-depth study of Java's new I/O API.