阻塞 IO
通常在进行同步 I/O 操作时,如果读取数据,代码会阻塞直至有可供读取的数据。同样,写入调用将会阻塞直至数据能够写入。传统的 Server/Client 模式会基于 TPR (Thread per Request ), 服务器会为每个客户端请求建立一个线程,由该线程单独负责处理一个客户请求。这种模式带来的问题就是线程数量的急剧增加,大量的线程会增大服务的开销。大多数的实现为了避免这个问题,采用了线程池模型,并设置线程池的最大数量,这个带来了新的问题,如果线程池中有 100 个线程,而有 100个用户都在进行大文件下载,会导致 101 个用户的请求无法及时处理,即便第 101 个用户指向请求一个几 kb 大小的页面。传统的 Server /Client 模式如下图所示
非阻塞 IO(NIO)
NIO 中非阻塞 I/O 采用了基于 Reactor 模式的工作方式, I/O 调用不会被阻塞, 相反是注册感兴趣的 I/O 事件,如可读数据到达,新的套接字连接等等,在发生特定的事件时,系统再通知我们。NIO 中实现非阻塞 I/O 的核心对象就是 Selector 。
Selector 就是注册各种 I / O 时间地方,而且当我们感兴趣的事件发生时,就是这个对象告诉我们发生的事件,如下图所示:
从图中可以看到,当有读,或者写等任何注册事件发生时,可以从 Selector 中获取响应的 SelectionKey , 同事从 SelectorKey 中恶意找到发生的事件和该事件所发生的具体 SelectableCannel 以获取客户端发送过来的数据。
非阻塞指的是 IO 本身不阻塞,但是获取 IO 事件的 select() 方法是需要阻塞等待的。区别阻塞的 IO 会阻塞在 IO 操作上,NIO 阻塞在事件获取上,没有事件就没有 IO , 从高层次来看 IO 就不阻塞了,也就是说只有 IO 已经发生那么我们才评估 IO 是否阻塞,但是 select() 阻塞的时候 IO 还没有发生,何谈 IO 的阻塞呢? NIO的本质是延迟 IO 操作到真正发生 IO的时候,而不是以前只要 IO 流打开就一直等待 IO 操作。
总结:NIO 不会发生 IO 打开后阻塞,而是在 IO 发生的时候会处理,其实本质也是一种 IO 延迟阻塞。
IO | NIO |
面向流(Stream Oriented) | 面向缓冲区(Buffer Oriented) |
阻塞IO(Blocking IO) | 非阻塞IO(Non Blocking IO) |
(无) | 选择器(Selector) |
NIO 概述
Java NIO 由一下几个核心部分组成:
- Channels
- Buffers
- Selectors
虽然 Java NIO 中除此之外还有很多类和组件,单 Channel, Buffer 和 Selector 构成了核心的 API 。其他组件,如 Pipe 和 FileLock , 只不过是与三个核心组件共同协作的工具类。
Channel
首先说下 Channel ,可以翻译为 “通道”Channel 和 IO 的Stream (流)是差不多的一个等级的。只是 Stream 是单向的,比如 InputStream , OutputStream. 而 Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作。
NIO 中的 Channel 的主要实现有:FileChannel、DatagramChannel 、SocketChannel 和 ServerSocketChannel , 这里看名字就可以猜出来: 分别对应文件 IO 、UDP 和 TCP (Server 和 Client)。
Buffer
NIO 找那个关键 Buffer 实现有:ByteBuffer , CharBuffer , DoubleBuffer , FloatBuffer, IntBuffer, LongBuffer, ShortBUffer , 分别对应基本数据类型 : byte , char , dubbole, float , int , long , short.
Selector
Selector 运行单线程处理多个 Channel , 如果你的引用中开启了多个通道,但是每个连接的流量都很低,使用 Selector 就会很方便,例如在一个聊天服务器中,要使用 Selector, 需要向 Selector 中注册 Channel , 然后调用 select() 方法。 这个方法一直会阻塞到其注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有:新的连接进来、数据接收等。