NIO

JavaNIO 非堵塞应用通常适于I/O 读写等方面, 我们知道, 系统运行的性能瓶颈通常在I/O读写,包括对网络和磁盘的操作上, 过去,在打开一个I/O通道后,read()将一直等待在端口一边读取字节内容,如果没有内容进来,read()也是傻傻的等,这会影响我们程序继续做其他事情, 那么改进做法就是开设线程, 让线程去等待, 但是这样做也是相当耗费资源的(线程调度)

Java NIO 非堵塞技术实际是采取 Reactor 模式, 或者说是 Observer 模式为我们监察I/O端口,如果有内容进来, 会自动通知我们,这样,我们就不必开启多个线程死等, 从外界看, 实现了流畅的I/O读写,不堵塞了。

Java NIO 出现不只是一个技术性能的提高,你会发现网络上到处在介绍它,因为它具有里程碑意义,从JDK1.4开始,Java开始提高性能相关的功能,从而使得Java在底层或者并行分布式计算等操作上已经可以和C或Perl等语言并驾齐驱。

IO模型主要分类

  • 同步 synchronous IO
  • 异步 asynchronous IO
  • 阻塞 blocking
  • 非阻塞 (non-blocking) NIO
  • 同步阻塞, blocking-IO, BIO
  • 同步非阻塞, non-blocking-IO, NIO
  • 异步阻塞: 不存在的…
  • 异步非阻塞, Asynchronous-non-blocking-IO, AIO

同步, 异步

同步和异步关注的是消息通信机制 ( synchronous communication/ asynchronous communication )

  • 同步 发送一个请求,等待返回, 再发送下一个请求,同步可以避免出现死锁,脏读的发生。
  • 异步 发送一个请求,不等待返回, 随时可以再发送下一个请求,可以提高效率,保证并发。

所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

举个通俗的例子: 你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下”,然后开始查啊查,等查好了 (可能是5秒,也可能是一天) 告诉你结果 (返回结果) 。而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了 (不返回结果) 。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。

阻塞和非阻塞

阻塞和非阻塞关注的是程序在等待调用结果 (消息,返回值) 时的状态.

  • 阻塞 传统的 IO 流都是阻塞式的。也就是说,当一个线程调用read()或者write()方法时,该线程将被阻塞,直到有一些数据读读取或者被写入,在此期间,该线程不能执行其他任何任务。在完成网络通信进行IO操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量的客户端时,性能急剧下降。

  • 非阻塞 Java NIO 是非阻塞式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程会去执行其他任务。线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。因此NIO可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。

IO, BIO, 同步阻塞 IO

IO 是面向流的, 数据的读取写入必须阻塞在一个线程内等待其完成。

经典的每连接每线程的模型, 之所以使用多线程, 主要原因在于 socket.accept(), socket.read(), socket.write() 三个主要函数都是同步阻塞的, 当一个连接在处理 I/O 的时候, 系统是阻塞的, 如果是单线程的话必然就挂死在那里;但 CPU 是被释放出来的, 开启多线程, 就可以让 CPU 去处理更多的事情。其实这也是所有使用多线程的本质:

  1. 利用多核。
  2. 当I/O阻塞系统, 但CPU空闲的时候, 可以利用多线程使用CPU资源。

现在的多线程一般都使用线程池, 可以让线程的创建和回收成本相对较低。 在活动连接数不是特别高 (小于单机1000) 的情况下, 这种模型是比较不错的, 可以让每一个连接专注于自己的 I/O 并且编程模型简单, 也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗, 可以缓冲一些系统处理不了的连接或请求。

不过,这个模型最本质的问题在于, 严重依赖于线程。但线程是很”贵”的资源, 主要表现在:

  1. 线程的创建和销毁成本很高, 在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。
  2. 线程本身占用较大内存, 像 Java 的线程栈, 一般至少分配512K~1M的空间, 如果系统中的线程数过千, 恐怕整个JVM的内存都会被吃掉一半。
  3. 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高 (超过20%以上), 导致系统几乎陷入不可用的状态。
  4. 容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。

所以, 当面对十万甚至百万级连接的时候, 传统的 BIO 模型是无能为力的。随着移动端应用的兴起和各种网络游戏的盛行, 百万级长连接日趋普遍, 此时, 必然需要一种更高效的I/O处理模型。

异步阻塞

异步不存在阻塞的情况

NIO, 同步非阻塞

NIO 是面向 buffer 的
以socket.read() 为例

传统的BIO 里面 socket.read(), 如果 TCP RecvBuffer 里没有数据, 函数会一直阻塞,直到收到数据,返回读到的数据。
对于NIO,如果 TCP RecvBuffer 有数据, 就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。

最新的 AIO 里面会更进一步: 不但等待就绪是非阻塞的, 就连数据从网卡到内存的过程也是异步的。

换句话说, BIO 里用户最关心“我要读”, NIO 里用户最关心”我可以读了”, 在 AIO 模型里用户更需要关注的是 “读完了”。

NIO 一个重要的特点是: socket主要的读、写、注册 和接收函数, 在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的 (消耗CPU, 但性能非常高) 。

结合事件模型使用 NIO 同步非阻塞特性

BIO 模型之所以需要多线程, 是因为在进行I/O操作的时候, 一是没有办法知道到底能不能写、能不能读,只能”傻等”, 即使通过各种估算, 算出来操作系统没有能力进行读写,也没法在 socket.read() 和 socket.write() 函数中返回,这两个函数无法进行有效的中断。所以除了多开线程另起炉灶,没有好的办法利用CPU。

NIO 的读写函数可以立刻返回,这就给了我们不开线程利用CPU的最好机会: 如果一个连接不能读写 ( socket.read() 返回0或者 socket.write() 返回0) ,我们可以把这件事记下来,记录的方式通常是在 selector 上注册标记位,然后切换到其它就绪的连接 (channel) 继续进行读写。

下面具体看下如何利用事件模型单线程处理所有I/O请求

NIO的主要事件有几个: 读就绪, 写就绪, 有新连接到来。

我们首先需要注册当这几个事件到来的时候所对应的处理器。然后在合适的时机告诉事件选择器: 我对这个事件感兴趣。对于写操作,就是写不出去的时候对写事件感兴趣;对于读操作,就是完成连接和系统没有办法承载新读入的数据的时;对于accept,一般是服务器刚启动的时候;而对于connect,一般是connect失败需要重连或者直接异步调用connect的时候。

其次,用一个死循环选择就绪的事件,会执行系统调用 (Linux 2.6之前是select、poll,2.6之后是 epoll, Windows是IOCP) ,还会阻塞的等待新事件的到来。新事件到来的时候, 会在 selector 上注册标记位, 标示可读、可写或者有连接到来。

注意, select 是阻塞的, 无论是通过操作系统的通知 (epoll) 还是不停的轮询(select,poll),这个函数是阻塞的。所以你可以放心大胆地在一个while(true)里面调用这个函数而不用担心CPU空转。

NIO 有一个主要的类 selector, 这个类似一个观察者, 只要我们把需要探知的 socketchannel告诉Selector, 我们接着做别的事情, 当有事件发生时, 他会通知我们, 传回一组SelectionKey,我们读取这些Key,就会获得我们刚刚注册过的 socketchannel, 然后, 我们从这个 channel 中读取数据, 放心,包准能够读到,接着我们可以处理这些数据。

Selector内部原理实际是在做一个对所注册的 channel 的轮询访问,不断的轮询(目前就这一个算法),一旦轮询到一个channel有所注册的事情发生,比如数据来了,他就会站起来报告,交出一把钥匙,让我们通过这把钥匙来读取这个channel的内容。

AIO (异步非阻塞 I/O 模型) , Asynchronous non blocking IO, Async I/O

异步非阻塞与同步非阻塞的区别在哪里? 异步非阻塞无需一个线程去轮询所有IO操作的状态改变, 在相应的状态改变后,系统会通知对应的线程来处理。对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。


http://ifeve.com/file-channel/
https://tech.meituan.com/2016/11/04/nio.html

作者: 卢毅luis 链接: https://www.zhihu.com/question/19732473/answer/20851256 来源: 知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

https://www.zhihu.com/question/19732473/answer/14413599
https://www.zhihu.com/question/19732473/answer/241673170

http://www.jdon.com/concurrent/nio%D4%AD%C0%ED%D3%A6%D3%C3.htm