Java NIO的工作方式

NIO的工作机制

为了了解NIO,我们先看一下NIO的相关类图,如下图所示:

上图中有两个关键类Channel和Selector,他们是Java NIO的核心。举个例子,我们把Channel比作高铁,则Selector就是高铁的调度系统,负责监控每列高铁的运行状态,是出站还是在路上,也就是说Selector可以轮询Channel的状态。还有一个Buffer类,可以将它比作高铁上的座位,至于是一等座还是二等座我们不得而知。了解了上面的例子,我们具体看一下NIO是如何工作的,下面是一段典型的NIO代码:

public void selector() throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //设置非阻塞方式
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.socket().bind(new InetSocketAddress(80));
        //注册监听事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while(true){
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> its = keys.iterator();
            while(its.hasNext()){
                SelectionKey selectionKey = its.next();
                if((selectionKey.readyOps() & SelectionKey.OP_ACCEPT) ==SelectionKey.OP_ACCEPT){
                    ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
                    //接收到请求
                    SocketChannel socketChannel = channel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector,SelectionKey.OP_READ);
                    its.remove();
                }else if((selectionKey.readyOps() & SelectionKey.OP_READ) ==SelectionKey.OP_READ){
                    SocketChannel sc = (SocketChannel) selectionKey.channel();
                    while(true){
                        buffer.clear();
                        int n = sc.read(buffer);
                        if(n<=0){
                            break;
                        }
                        buffer.flip();
                    }
                    its.remove();
                }
            }
        }
    }

调用Selector的静态方法创建一个Selector,创建一个服务端的Channel绑定到Socket对象上,把这个通信信道注册到选择器上并设置为非阻塞模式。然后调用Selector的selectedKeys方法检查注册到选择器上的所有通信信道是否有感兴趣的事件发生。如果有事件发生则返回所有的SelectionKey,通过SelectionKey的channel方法可以取得通信信道,从而读取通信数据。

上图描述了基于NIO的Socket请求处理过程。Selector可以同时监听一组通信信道上的IO状态,前提是这些通信信道已经注册到Selector上。Selector可以调用select方法检查通信信道上的IO是否已经准备好,如果监听的所有通信信道上没有状态变化,那么select方法会阻塞或则超时返回0。如果有多个通信信道有数据,则把这些数据分配到对应的Buffer中。所以关键的地方是,有一个线程处理所有链接的数据交互,每个链接的数据交互都不是阻塞的,所以可以同事处理大量的请求。

Buffer的工作方式

可以把Buffer理解为一组基本数据类型的元素列表 ,它通过几个变量保存这些数据的当前位置状态,也就是有四个索引。

  • capacity:缓存区数据的总长度
  • position:下一个要操作的元素的位置
  • limit:缓存区中不可操作的下一个元素的位置limit<=capacity
  • mark:用于记录当前position的前一个位置或则默认是0

他们的关系如下图:

我们通过ByteBuffer.allocate(11)创建一个11个字节的数组缓存区,出事状态下position为0,capacity和limit都是默认长度,如上图。

当我们向Buffer中添加五个元素后,position的值变为5,而limit和capacity的值不变,如下图:

当我们调用buffer.flip()方法后,position变为0,limit变为5,capacity不变,如下图:这时候底层操作系统就可以从Buffer中读取5个字节并发射出去。

当下一次写数据之前,调用clear方法,缓冲区的索引状态又回到初始状态。当我们调用mark()方法的时候,他将记录当前position的前一个位置,调用reset后,position将恢复mark记录下的位置。

有一点需要说明,通过Channel获取的IO数据首先要经过操作系统的Socket缓冲区,再将数据复制到Buffer中,这个操作系统缓冲区就是TCP关联的RECEQ和SENDQ队列,从操作系统缓冲区到用户缓冲区的数据复制比较耗性能,BUffer提供了一种直接操作操作系统缓冲区的方式ByteBuffer.allocateDirect,这个方法返回的DirectByteBuffer就是关联的底层操作系统缓冲区,他通过Native代码操作非JVM堆的内存空间,每次的创建和释放都调用System.gc,容易引起JVM内存泄露问题。

NIO数据访问方式

NIO提供过了比传统的文件访问方式更好的文件访问方式

  • FileChannel.transferFrom,FileChannel.transferTo
  • FileChannel.map

FileChannel.transferXXX

FileChannel.transferXXX相比传统方法可以减少从内核到用户空间的复制,数据直接在内核中移动,在Linux中使用的是sendfile系统调用。

FileChannel.map

FileChannel.map按照一定的大小块把文件映射为内存区域,程序访问内存区域的时候,直接访问文件系统,这种方式省去了从内核空间到用户空间的复制的损耗,适合对大文件的只读操作。但是这种方式与操作系统的底层实现有关。

posted @ 2018-01-09 19:56  木易森林  阅读(768)  评论(0编辑  收藏  举报