传统IO在读写数据到一个具体位置(可能是文件、网络主机)时,读写操作需要分别对应一个流。而NIO是通过channel的方式连接具体位置,且channel是双向,即可用来读,也可用来写
传统的流,读取时,直接从流中即可获取内容,写时,直接向流写内容即可。而NIO中,channel读写需要通过Buffer间接进行。
一段典型的IO代码如下:
import java.io.*; public class CopyFile { public static void main(String args[]) throws IOException { FileInputStream in = null; FileOutputStream out = null; try { in = new FileInputStream("input.txt"); out = new FileOutputStream("output.txt"); int c; while ((c = in.read()) != -1) { out.write(c); } }finally { if (in != null) { in.close(); } if (out != null) { out.close(); } } } }
一段典型NIO代码如下:
RandomAccessFile aFile = new RandomAccessFile("/home/vincent/Temp/test_nio", "rw"); FileChannel inChannel = aFile.getChannel(); ByteBuffer buf = ByteBuffer.allocate(48); //从文件中读 int bytesRead = inChannel.read(buf);//读数据先要读到buffer while (bytesRead != -1) { System.out.println("Read " + bytesRead); buf.flip(); while(buf.hasRemaining()){ System.out.print((char) buf.get());//再从buffer中输出结果 } buf.clear(); bytesRead = inChannel.read(buf); } aFile.close();
传统IO在通过stream读写时,会阻塞当前线程,一定要操作完成后才能返回。比如InputStream的read方法就规定,在流结束或异常之前,如果没有读到数据就阻塞。而NIO的Buffer是非阻塞式的,调用后,立马返回,当前buffer中有什么就是什么。
不同于IO直接通过流进行读写。NIO在读写时,都需要channel和buffer两个组件来配合使用,才能完成读写操作。
上图以对文件读写为例。
由于NIO中,channel和buffer都是双向的,即可用来读,也可用来写。所以只需要创建一个channel和buffer
读时
Buffer的一系列方法,无非就是在操控上面的三个指针,从而达到灵活读写的目的。以ByteBuffer举例,典型的指针操作场景如下
从左至右介绍
读模式倒带
将position恢复到初始位置,重新读取
position已经读到了1位置,调用buffer rewind方法后,position会被重置到初始位置,也既是0。这样buffer中的内容,又可以重新读取
写模式倒带
将position恢复到初始位置,重新写,会造成之前的写丢失。这里就不画图了
在调用buffer的mark方法后,再调用reset方法,可以实现类似rewind的倒带操作,并都可用于读写。不同的是,rewind倒带时,会将position指针重置到初始位置。而reset则会将指针重置到mark方法之前标记的位置
以上图为读模式下的buffer做举例说明,从左至右
public void testNio() throws IOException { RandomAccessFile aFile = new RandomAccessFile("/home/vincent/Temp/test_nio", "rw"); FileChannel inChannel = aFile.getChannel(); ByteBuffer buf = ByteBuffer.allocate(48); //从文件中读 int bytesRead = inChannel.read(buf); while (bytesRead != -1) { System.out.println("Read " + bytesRead); buf.flip(); while(buf.hasRemaining()){ System.out.print((char) buf.get()); } buf.clear(); bytesRead = inChannel.read(buf); } //写数据到文件: buf.clear(); buf.put("/nhello java.".getBytes()); buf.flip(); while(buf.hasRemaining()) { inChannel.write(buf); } aFile.close(); }
test_nio文件之前有一句文本“hello world”
以上方法使用了一个channel和一个buffer,完成了从test_nio读数据输出后,再向文件写如了一句“hello java”。程序执行完毕后,可以在文件中看到“hello world”和“hello java”两句话。这段代码展示了channel和buffer的双向性,同时也展示了buffer在读写两种模式下的切换方法。
我们知道,每个线程需要占用一定资源,同时线程间切换,也需要耗费资源。所以如果对每个channel都创建一个线程的话,在某些场景下是低效的。比如通过NIO实现一个网络服务器,由于可能面对巨大的并发量,显然一个channel对应一个线程是不可取的。而selector是一种多路复用器技术,通过selector的单线程,实现对多个channel状态的监听,从而降低线程数量。selector的机制图如下:
使用selector的典型代码如下:
Selector selector = Selector.open(); channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ); while(true) { int readyChannels = selector.select();//这一步会阻塞 if(readyChannels == 0) continue; Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while(keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if(key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel. } else if (key.isConnectable()) { // a connection was established with a remote server. } else if (key.isReadable()) { // a channel is ready for reading } else if (key.isWritable()) { // a channel is ready for writing } keyIterator.remove(); } }
上述代码大致来看,首先先将channel注册到selector,然后selector通过select方法返回已经ready的channel数量,如果没有任何ready,则阻塞。如果有ready的channel,则通过遍历SelectionKey,依次判定期望的事件是否发生。如果是,则执行相关业务操作
http://tutorials.jenkov.com/java-nio/index.html