了解Java NIO前,先参考磁盘IO:缓存IO、直接IO、内存映射及网络IO模型。
1、网络IO
BIO就是基于Thread per Request的传统server/client实现模式(阻塞I/O);
NIO通常采用Reactor模式,当有事件触发时,我们得到通知,进行相应的处理(select加非阻塞I/O);AIO通常采用Proactor模式,AIO进行I/O操作,都是异步处理,当事件完成时,我们会得到通知(异步IO)。
(1)NIO
NIO最主要的就是实现了对I/O复用及非阻塞I/O操作的支持。其中一种通过把一个套接字通道(SocketChannel)注册到一个选择器(Selector)中,不时调用后者的选择(select)方法就能返回满足的选择键(SelectionKey),键中包含了SOCKET事件信息。这就是select模型。
Reactor负责监听server socket,accept新连接,并将建立的socket分派给Acceptor;Acceptor负责多路分离已连接的socket,读写网络数据。
通常会将上述过程以以下两个过程处理:
1)一个线程专门负责监听客户端的连接请求,而且是以阻塞方式执行的;
2)另外多个线程专门负责处理请求。
像Web服务器Tomcat和Jetty都使用这样的处理方式。
另外,可以将数据处理线程(完成业务功能)做成一个线程池,这样数据读出后,立即扔到线程池中,这样加速处理速度。
Reactor代码:
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; public class Reactor implements Runnable { private Selector selector = null; private ServerSocketChannel serverSocket = null; public Reactor(int port) { try { selector = Selector.open(); serverSocket = ServerSocketChannel.open(); InetSocketAddress address = new InetSocketAddress("localhost", port); serverSocket.socket().bind(address); // 设置为non-blocking的方式。 serverSocket.configureBlocking(false); // 向selector注册该channel SelectionKey sk = serverSocket.register(selector, SelectionKey.OP_ACCEPT); // 利用sk的attache功能绑定Acceptor 如果有事情,触发Acceptor sk.attach(new Acceptor()); System.out.println("服务器启动正常!"); } catch (IOException e) { System.out.println("启动服务器时出现异常!"); e.printStackTrace(); } } public void run() { // normally in a new Thread try { while (!Thread.interrupted()) { selector.select(); Iterator<SelectionKey> it = selector.selectedKeys().iterator(); // Selector如果发现channel有OP_ACCEPT或READ事件发生,下列遍历就会进行。 while (it.hasNext()) { // 来一个事件 第一次触发一个accepter线程,以后触发SocketReadHandler SelectionKey selectionKey = it.next(); dispatch((Runnable) selectionKey.attachment()); it.remove(); } } } catch (IOException e) { System.out.println("reactor stop!" + e); } } // 运行Acceptor或SocketReadHandler void dispatch(Runnable r) { if (r != null) { r.run(); } } public static void main(String[] args) { new Thread(new Reactor(8888)).start(); } class Acceptor implements Runnable { // inner public void run() { try { SocketChannel c = serverSocket.accept(); if (c != null) { System.out.println("接收到来自客户端(" + c.socket().getInetAddress().getHostAddress() + ")的连接"); // 调用Handler来处理channel new SocketReadHandler(selector, c); } } catch (IOException e) { System.out.println("accept stop!" + e); } } } }
import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; public class SocketReadHandler implements Runnable { private static final int READ_STATUS = 1; private static final int WRITE_STATUS = 2; private SocketChannel socketChannel; private SelectionKey selectionKey; private int status = READ_STATUS; public SocketReadHandler(Selector selector, SocketChannel socketChannel) { this.socketChannel = socketChannel; try { socketChannel.configureBlocking(false); selectionKey = socketChannel.register(selector, 0); //将SelectionKey绑定为本Handler。下一步有事件触发时,将调用本类的run方法。 selectionKey.attach(this); //同时将SelectionKey标记为可读,以便读取。 selectionKey.interestOps(SelectionKey.OP_READ); selector.wakeup(); } catch (IOException e) { e.printStackTrace(); } } public void run() { try { if (status == READ_STATUS) { read(); selectionKey.interestOps(SelectionKey.OP_WRITE); status = WRITE_STATUS; } else if (status == WRITE_STATUS) { process(); selectionKey.cancel(); System.out.println("服务器发送消息成功!"); } } catch (IOException e) { e.printStackTrace(); } } public void read() throws IOException { ByteBuffer buffer = ByteBuffer.allocate(1024); socketChannel.read(buffer); System.out.println("接收到来自客户端(" + socketChannel.socket().getInetAddress().getHostAddress() + ")的消息:" + new String(buffer.array())); } public void process() throws IOException { String content = "Hello World!"; ByteBuffer buffer = ByteBuffer.wrap(content.getBytes()); socketChannel.write(buffer); } }
注意在Handler里面又执行了一次attach,这样,覆盖前面的Acceptor,下次该Handler又有READ事件发生时,将直接触发Handler。
Netty是一个基于NIO的客户、服务器端编程框架,使用Netty可以确保你快速和简单的开发出一个网络应用。
同Netty类似,MINA也是一个基于NIO的框架。
另外,也可以看看memcached的线程模型
它采用较典型的Master-Worker模型:
1)主线程负责监听客户端的建立连接请求,以及accept 连接,将连接好的套接字放入连接队列;
2)调度workers空闲线程来负责处理已经建立好的连接的读写等事件。
(2)异步I/O
AIO简化了程序的编写,stream的读取和写入都由OS来完成,不需要像NIO那样子遍历Selector。Windows基于IOCP实现AIO,Linux只有eppoll模拟实现了AIO。Java7之前的JDK只支持NIO和BIO,从7开始支持AIO。
异步通道一般提供两种使用方式:
1)一种是通过Java同步工具包中的java.util.concurrent.Future类的对象来表示异步操作的结果;
2)另一种是在操作时传入一个java.nio.channels.CompletionHandler接口实现对象来作为操作完成时的回调方法。
这两种使用的区别在于调用者通过何种方式来使用异步的结果。在使用Future类对象时,要求调用者在合适的时机显式地通过Future类对象的get方法来得到实际的操作结果;而在使用CompletionHandler接口时,实际的调用结果作为回调方法的参数来给出。
服务端代码:
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousServerSocketChannel; import java.nio.channels.AsynchronousSocketChannel; import java.nio.channels.CompletionHandler; import java.util.concurrent.ExecutionException; public class SimpleServer { public SimpleServer(int port) throws IOException { final AsynchronousServerSocketChannel listener = AsynchronousServerSocketChannel .open().bind(new InetSocketAddress(port)); listener.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() { public void completed(AsynchronousSocketChannel ch, Void att) { // 接受下一个连接 listener.accept(null, this); // 处理当前连接 handle(ch); } public void failed(Throwable exc, Void att) { } }); } public void handle(AsynchronousSocketChannel ch) { ByteBuffer byteBuffer = ByteBuffer.allocate(32); try { ch.read(byteBuffer).get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } byteBuffer.flip(); System.out.println(byteBuffer.get()); } }
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousSocketChannel; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; public class SimpleClient { private AsynchronousSocketChannel client; public SimpleClient(String host, int port) throws IOException, InterruptedException, ExecutionException { this.client = AsynchronousSocketChannel.open(); Future<?> future = client.connect(new InetSocketAddress(host, port)); future.get(); } public void write(byte b) { ByteBuffer byteBuffer = ByteBuffer.allocate(32); byteBuffer.put(b); byteBuffer.flip(); client.write(byteBuffer); } }
import java.io.IOException; import java.util.concurrent.ExecutionException; public class AIOTest { public void testServer() throws IOException, InterruptedException { SimpleServer server = new SimpleServer(7788); Thread.sleep(5000); } public void testClient() throws IOException, InterruptedException, ExecutionException { SimpleClient client = new SimpleClient("localhost", 7788); client.write((byte) 11); } public static void main(String[] args) throws Exception{ AIOTest at = new AIOTest(); at.testServer(); at.testClient(); } }
因为是异步的,所以在运行server的时候没有发生同步阻塞,在这里我加了一个线程sleep(),如果没有的话,程序会直接跑完回收掉。
3、磁盘IO(ByteBuffer)
SocketChannel的读写是通过一个类叫ByteBuffer来操作的(与传统面向stream方式一次一个字节处理数据相比,它是按块处理数据,比流式的快得多),这个类本身的设计是不错的,比直接操作byte[]方便多了。ByteBuffer有两种模式:间接(HeapByteBuffer)和直接(DirectByteBuffer)模式。
(1)间接模式:HeapByteBuffer
即操作堆内存 (byte[])。
(2)直接模式:DirectByteBuffer
它通过Native代码操作非JVM堆的内存空间。该内存块并不直接由Java虚拟机负责垃圾收集,但是在DirectByteBuffer包装类被回收时,会通过Java Reference机制来会调用相应的JNI方法释放本地内存,所以本地内存的释放也依赖于JVM中DirectByteBuffer对象的回收。
直接模式存在的问题:
由于垃圾回收本身成本较高,一般JVM在堆内存未耗尽时,不会进行垃圾回收操作。我们知道在32位机器上,每个进程的最大可用内存为4G,用户可用内存在大概为3G左右,如果为堆分配过大的内存时,本地内存可用空间就会相应减少。当我们为堆分配较多的内存时,JVM可能会在相当长的时间内不会进行垃圾回收操作,从而本地内存不断分配,无法释放,最终导致OutOfMemoryError。
由此可见,在使用直接缓冲区之前,需要做出权衡:
1)堆缓冲区的性能已经相当高,若无必要,使用堆缓冲区足矣。若确实有提升性能的必要时,再考虑使用本地缓冲区。
2)为JVM分配堆内存时,并不是越大越好,堆内存越大,本地内存就越小,根据具体情况决定,主要针对32位机器,64位机器上不存在该问题。
(3)内存映射
Java中使用MappedByteBuffer来提供内存映射功能,可以把整个文件(不管文件有多大)看成是一个ByteBuffer。MappedByteBuffer是ByteBuffer的子类。 MappedByteBuffer将文件直接映射到内存。通常可以映射整个文件,如果文件比较大的话可以分段进行映射,只要指定文件的哪个部分就可以。
按理来说,MappedByteBuffer应该是DirectByteBuffer的子类,以下是JDK源码中对该情况作的解释:
// This is a little bit backwards: By rights MappedByteBuffer should be a
// subclass of DirectByteBuffer, but to keep the spec clear and simple, and
// for optimization purposes, it's easier to do it the other way around.
// This works because DirectByteBuffer is a package-private class.
import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileChannel.MapMode; public class FileChannelStudy { public static void main(String[] args) throws IOException { ByteBuffer byteBuf = ByteBuffer.allocate(483464 * 1024); byte[] bbb = new byte[483464 * 1024]; FileInputStream fis = new FileInputStream("f://a.rmvb"); FileOutputStream fos = new FileOutputStream("f://aa.rmvb"); FileChannel fc = fis.getChannel(); long timeStar = System.currentTimeMillis();// 得到当前的时间 fc.read(byteBuf);//1 读取 //MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size()); long timeEnd = System.currentTimeMillis();// 得到当前的时间 System.out.println("Read time :" + (timeEnd - timeStar) + "ms"); timeStar = System.currentTimeMillis(); fos.write(bbb);// 写入 //mbb.flip(); timeEnd = System.currentTimeMillis(); System.out.println("Write time :" + (timeEnd - timeStar) + "ms"); fos.flush(); fc.close(); fis.close(); } }
运行结果:
Read time :913ms
我们把标注1和2语句注释掉,换成它们下面的被注释的那条语句,再来看运行效果:
Read time :2ms
Write time :0ms
可以看出速度有了很大的提升。MappedByteBuffer的确快,但也存在一些问题,主要就是内存占用和文件关闭等不确定问题。被MappedByteBuffer打开的文件只有在垃圾收集时才会被关闭,而这个点是不确定的。
如使用MD5进行大文件验证:
FileInputStream in = new FileInputStream(file); FileChannel ch = in.getChannel(); MappedByteBuffer byteBuffer = ch.map(FileChannel.MapMode.READ_ONLY, 0, file.length()); messageDigest.update(byteBuffer); String md5 = bufferToHex(messageDigest.digest()); ch.close(); in.close();本来想如果文件md5与数据库存储的值不同就删掉该文件的,结果出现了文件无法删除的情况。抛出的异常:
解决方法:
AccessController.doPrivileged(new PrivilegedAction() { public Object run() { try { Method getCleanerMethod = buffer.getClass().getMethod("cleaner", new Class[0]); getCleanerMethod.setAccessible(true); sun.misc.Cleaner cleaner = (sun.misc.Cleaner) getCleanerMethod.invoke(byteBuffer, new Object[0]); cleaner.clean(); } catch (Exception e) { e.printStackTrace(); } return null; } });
MongoDB也是使用的内存映机制,它会把数据文件映射到内存中,如果是读操作,内存中的数据起到缓存的作用,如果是写操作,内存还可以把随机的写操作转换成顺序的写操作,总之可以大幅度提升性能。MongoDB并不干涉内存管理工作,而是把这些工作留给操作系统的虚拟内存管理器去处理,这样做的好处是简化了MongoDB的工作,但坏处是你没有方法很方便的控制MongoDB占多大内存,幸运的是虚拟内存管理器的存在让我们多数时候并不需要关心这个问题。
参考:
NIO 入门:http://www.ibm.com/developerworks/cn/education/java/j-nio/
java模式之Reactor:http://www.blogjava.net/baoyaer/articles/87514.html
Java AIO 入门实例:http://www.xiaoyaochong.net/wordpress/?p=73
JDK7新特性<八>异步io/AIO:http://www.iamcoding.com/?p=130
DirectBuffer及内存泄漏:http://blog.csdn.net/zhouhl_cn/article/details/6573213
慎用 MappedByteBuffer:http://yipsilon.iteye.com/blog/298153
用MappedByteBuffer进行特大文件拷贝分割:http://ilexes.blog.51cto.com/705330/341157
java nio 之MappedByteBuffer,高效文件/内存映射:http://www.zhurouyoudu.com/index.php/archives/470/