IO模型与NIO模型简介

IO和NIO比较

同步与异步

同步I/O 每个请求必须逐个地被处理,一个请求的处理会导致整个流程的暂时等待,这些事件无法并发地执行。用户线程发起I/O请求后需要等待或者轮询内核I/O操作完成后才能继续执行。

异步I/O 多个请求可以并发地执行,一个请求或者任务的执行不会导致整个流程的暂时等待。用户线程发起I/O请求后仍然继续执行,当内核I/O操作完成后会通知用户线程,或者调用用户线程注册的回调函数。

阻塞与非阻塞

阻塞 某个请求发出后,由于该请求操作需要的条件不满足,请求操作一直阻塞,不会返回,直到条件满足。Java IO的各种流都是阻塞的。这意味着一个线程一旦调用了read(),write()方法,那么该线程就被阻塞住了,知道读取到数据或者数据完整写入了。在此期间线程不能做其他任何事情。

非阻塞 请求发出后,若该请求需要的条件不满足,则立即返回一个标志信息告知条件不满足,而不会一直等待。一般需要通过循环判断请求条件是否满足来获取请求结果。Java NIO的非阻塞模式使得线程可以通过channel来读数据,并且是返回当前已有的数据,或者什么都不返回如果没有数据可读的话。这样一来线程不会被阻塞住,它可以继续向下执行。通常线程在调用非阻塞操作后,会通知处理其他channel上的IO操作。因此一个线程可以管理多个channel的输入输出。

 

向流和面向缓冲区比较(Stream Oriented vs. Buffer Oriented)

第一个重大差异是Java IO是面向流的,而Java NIO是面向缓存区的。

面向流 意思是我们每次从流当中读取一个或多个字节。怎么处理读取到的字节是我们自己的事情。他们不会再任何地方缓存。再有就是我们不能在流数据中向前后移动。如果需要向前后移动读取位置,那么我们需要首先为它创建一个缓存区。

面向缓冲区 这有些细微差异。数据是被读取到缓存当中以便后续加工。我们可以在缓存中向向后移动。这个特性给我们处理数据提供了更大的弹性空间。当然我们任然需要在使用数据前检查缓存中是否包含我们需要的所有数据。另外需要确保在往缓存中写入数据时避免覆盖了已经写入但是还未被处理的数据。

 

总结

面向流和面向缓冲

ava IO是面向流的,每次从流(InputStream/OutputStream)中读一个或多个字节,直到读取完所有字节,它们没有被缓存在任何地方。另外,它不能前后移动流中的数据,如需前后移动处理,需要先将其缓存至一个缓冲区。

Java NIO面向缓冲,数据会被读取到一个缓冲区,需要时可以在缓冲区中前后移动处理,这增加了处理过程的灵活性。但与此同时在处理缓冲区前需要检查该缓冲区中是否包含有所需要处理的数据,并需要确保更多数据读入缓冲区时,不会覆盖缓冲区内尚未处理的数据。

阻塞和非阻塞

Java IO的各种流是阻塞的。当某个线程调用read()或write()方法时,该线程被阻塞,直到有数据被读取到或者数据完全写入。阻塞期间该线程无法处理任何其它事情。

Java NIO为非阻塞模式。读写请求并不会阻塞当前线程,在数据可读/写前当前线程可以继续做其它事情,所以一个单独的线程可以管理多个输入和输出通道。

选择器

Java NIO的选择器允许一个单独的线程同时监视多个通道,可以注册多个通道到同一个选择器上,然后使用一个单独的线程来“选择”已经就绪的通道。这种“选择”机制为一个单独的线程管理多个通道提供了可能。

零拷贝

Java NIO中提供的FileChannel拥有transferTo和transferFrom两个方法,可直接把FileChannel中的数据拷贝到另外一个Channel,或者直接把另外一个Channel中的数据拷贝到FileChannel。该接口常被用于高效的网络/文件的数据传输和大文件拷贝。在操作系统支持的情况下,通过该方法传输数据并不需要将源数据从内核态拷贝到用户态,再从用户态拷贝到目标通道的内核态,同时也避免了两次用户态和内核态间的上下文切换,也即使用了“零拷贝”,所以其性能一般高于Java IO中提供的方法,使用FileChannel的零拷贝把本地文件内容传输到网络。

NIO

NIO包含下面几个核心的组件:

  • Channels
  • Buffers
  • Selectors

Selectors

Selector是Java NIO中的一个组件,用于检查一个或多个NIO Channel的状态是否处于可读、可写。如此可以实现 单线程管理多个channels,也就是可以管理多个网络链接。 

Java NIO的selector允许一个单一线程监听多个channel输入。我们可以注册多个channel到selector上,然后然后用一个线程来挑出一个处于可读或者可写状态的channel。selector机制使得单线程管理过个channel变得容易。

这有一幅示意图,描述了单线程处理三个channel的情况:

overview-selectors.png


创建一个Selector 可以通过Selector.open()方法: 

Selector selector = Selector.open();       


注册Channel到Selector上  

为了同Selector挂了Channel,我们必须先把Channel注册到Selector上,这个操作使用SelectableChannel.register():
channel.configureBlocking( false );
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

Channel必须是非阻塞的。所以FileChannel不适用Selector,因为FileChannel不能切换为非阻塞模式。Socket c hannel可以正常使用。

注意register的第二个参数,这个参数是一个“关注集合”,代表我们关注的channel状态,有四种基础类型可供 监听:

  • Connect
  • Accept
  • Read
  • Write

一个channel触发了一个事件也可视作该事件处于就绪状态。因此当channel与server连接成功后,那么就是“连 接就绪”状态。server channel接收请求连接时处于“可连接就绪”状态。channel有数据可读时处于“读就 绪”状态。channel可以进行数据写入时处于“写就绪”状态。

上述的四种就绪状态用SelectionKey中的常量表示如下:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE
如果对多个事件感兴趣可利用位的或运算结合多个常量,

 

int  interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;


SelectionKey's 

 

register方法把Channel注册到了Selectors上,这个方法的返回值是SelectionKeys,这个返回的对象包含了一些比较有价值的属性:

  •  The interest set
     
    这个“关注集合”实际上就是我们希望处理的事件的集合,它的值就是注册时传入的参数,我们可以用按位与运算把每个事件取出来 
     
    int  interestSet = selectionKey.interestOps();
     
    boolean  isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
    boolean  isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
    boolean  isInterestedInRead    = interestSet & SelectionKey.OP_READ;
    boolean  isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;
  •  The ready set
     

    "就绪集合"中的值是当前channel处于就绪的值,一般来说在调用了select方法后都会需要用到就绪状态。

     

    int  readySet = selectionKey.readyOps();

    或者:

     

    selectionKey.isAcceptable();
    selectionKey.isConnectable();
    selectionKey.isReadable();
    selectionKey.isWritable();


  •  The Channel
  •  The Selector
     
    从SelectionKey操作Channel和Selector:
    Channel channel = selectionKey.channel();
    Selector selector = selectionKey.selector();
  •  An attached object (optional) 
    可以给一个SelectionKey附加一个Object,这样做一方面可以方便我们识别某个特定的channel,同时也增加 了channel相关的附加信息。例如,可以把用于channel的buffer附加到SelectionKey上:

    selectionKey.attach(theObject); Object attachedObj = selectionKey.attachment();

    附加对象的操作也可以在register的时候就执行

    SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

NIO Channel通道

 

特点

 

  • 通道可以读也可以写,流一般来说是单向的(只能读或者写)。
  • 通道可以异步读写。
  • 通道总是基于缓冲区Buffer来读写。
  • 我们可以从通道中读取数据,写入到buffer;也可以中buffer内读数据,写入到通道中。

Channel实现

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

FileChannel用于文件的数据读写。 DatagramChannel用于UDP的数据读写。 SocketChannel用于TCP的数据读写。 ServerSocketChannel允许我们监听TCP链接请求,每个请求会创建会一个SocketChannel.

NIO Buffer缓冲区

buffer本质上就是一块内存区,可以用来写入数据,并在稍后读取出来。这块内存被NIO Buffer包裹起来,对外提供一系列的读写方便开发的接口。

Buffer基本用法(Basic Buffer Usage)

利用Buffer读写数据,通常遵循四个步骤:

  • 把数据写入buffer;
  • 调用flip;
  • 从Buffer中读取数据;
  • 调用buffer.clear()或者buffer.compact()
Buffer的容量,位置,上限

一个Buffer有三个属性是必须掌握的,分别是:

  • capacity容量
  • position位置
  • limit限制

position和limit的具体含义取决于当前buffer的模式。capacity在两种模式下都表示容量。

buffers-modes.png

容量(Capacity)

作为一块内存,buffer有一个固定的大小,叫做capacity容量。也就是最多只能写入容量值得字节,整形等数据。一旦buffer写满了就需要清空已读数据以便下次继续写入新的数据。

位置(Position)

当写入数据到Buffer的时候需要中一个确定的位置开始,默认初始化时这个位置position为0,一旦写入了数据比如一个字节,整形数据,那么position的值就会指向数据之后的一个单元,position最大可以到capacity-1.

当从Buffer读取数据时,也需要从一个确定的位置开始。buffer从写入模式变为读取模式时,position会归零,每次读取后,position向后移动。

上限(Limit)

在写模式,limit的含义是我们所能写入的最大数据量。它等同于buffer的容量。

一旦切换到读模式,limit则代表我们所能读取的最大数据量,他的值等同于写模式下position的位置。

数据读取的上限时buffer中已有的数据,也就是limit的位置(原position所指的位置)。

Buffer Types

Java NIO有如下具体的Buffer类型:

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

 

IO

Java的四种IO模型

  • 阻塞 I/O
  • 非阻塞 I/O
  • I/O 多路复用(select和poll)
  • 异步 I/O(Posix.1的aio_系列函数)

阻塞IO 请求无法立即完成则保持阻塞,一般分为两个阶段:

  • 阶段1:等待数据就绪。网络 I/O 的情况就是等待远端数据陆续抵达;磁盘I/O的情况就是等待磁盘数据从磁盘上读取到内核态内存中。
  • 阶段2:数据拷贝。出于系统安全,用户态的程序没有权限直接读取内核态内存,因此内核负责把内核态内存中的数据拷贝一份到用户态内存中。

非阻塞IO 有三个阶段:

  • socket设置为 NONBLOCK(非阻塞)就是告诉内核,当所请求的I/O操作无法完成时,不要将线程睡眠,而是返回一个错误码(EWOULDBLOCK) ,这样请求就不会阻塞。
  • I/O操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。整个I/O 请求的过程中,虽然用户线程每次发起I/O请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的 CPU 的资源。
  • 数据准备好了,从内核拷贝到用户空间。

IO多路复用 会用到select和poll函数,这两个函数会使线程阻塞,但是和IO阻塞不同的是,这两个函数可以同时阻塞读个IO操作。还可以同时对多个读操作多个写操作的IO函数进行检测,知道有数据可读或者可写时,才真正调用IO操作函数。

从流程上来看,使用select函数进行I/O请求和同步阻塞模型没有太大的区别,甚至还多了添加监视Channel,以及调用select函数的额外操作,增加了额外工作。但是,使用 select以后最大的优势是用户可以在一个线程内同时处理多个Channel的I/O请求。用户可以注册多个Channel,然后不断地调用select读取被激活的Channel,即可达到在同一个线程内同时处理多个I/O请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

调用select/poll该方法由一个用户态线程负责轮询多个Channel,直到某个阶段1的数据就绪,再通知实际的用户线程执行阶段2的拷贝。 通过一个专职的用户态线程执行非阻塞I/O轮询,模拟实现了阶段一的异步化。

异步IO 调用aio_read 函数,告诉内核描述字,缓冲区指针,缓冲区大小,文件偏移以及通知的方式,然后立即返回。当内核将数据拷贝到缓冲区后,再通知应用程序。所以异步I/O模式下,阶段1和阶段2全部由内核完成,完成不需要用户线程的参与。



相关

NIO和IO是如何影响程序设计的(How NIO and IO Influences Application Design)

开发中选择NIO或者IO会在多方面影响程序设计:

  1. 使用NIO、IO的API调用类
  2. 数据处理
  3. 处理数据需要的线程数

 

API调用(The API Calls)

显而易见使用NIO的API接口和使用IO时是不同的。不同于直接冲InputStream读取字节,我们的数据需要先写入到buffer中,然后再从buffer中处理它们。

 

IO和NIO适用场景

如果你需要同时管理成千上万的链接,这些链接只发送少量数据,例如聊天服务器,用NIO来实现这个服务器是有优势的。类似的,如果你需要维持大量的链接,例如P2P网络,用单线程来管理这些 链接也是有优势的。这种单线程多连接的设计可以用下图描述:

nio-vs-io-3.png

Java NIO: A single thread managing multiple connections

如果链接数不是很多,但是每个链接的占用较大带宽,每次都要发送大量数据,那么使用传统的IO设计服务器可能是最好的选择。下面是经典IO服务设计图:

nio-vs-io-4.png

阻塞IO下的服务器实现

单个线程逐个处理所有请求:使用阻塞I/O的服务器,一般使用循环,逐个接受连接请求并读取数据,然后处理下一个请求。

这个例子使用单个线程处理所有请求,同一时间只能处理一个请求,等待IO过程浪费大量的CPU资源,同时无法充分使用多CPU的优势。

为每个请求创建一个线程:


为了防止连接请求过多,导致服务器的线程数过多,造成多线程上下文切换开销。可以通过线程池限制创建的线程数,

经典Reactor模式:

在Reactor模式中,有三个部分:

  • Reactor 将I/O事件发派给对应的Handler
  • Acceptor 处理客户端连接请求
  • Handlers 执行非阻塞读/写

 

参考:http://www.jasongj.com/java/nio_reactor/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值