资深架构师总结:彻底搞懂NIO效率高的原理( 二 )


资深架构师总结:彻底搞懂NIO效率高的原理

文章插图
 
Java NIO里关键的Buffer实现:
  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer
【资深架构师总结:彻底搞懂NIO效率高的原理】这些Buffer覆盖了你能通过IO发送的基本数据类型: byte、short、int、long、float、double和char 。
为了理解Buffer的工作原理 , 需要熟悉它的三个属性:
  • capacity
  • position
  • limit
position和limit的含义取决于Buffer处在读模式还是写模式 。不管Buffer处在什么模式 , capacity的含义总是一样的 。
资深架构师总结:彻底搞懂NIO效率高的原理

文章插图
 
capacity
作为一个内存块 , Buffer有个固定的最大值 , 就是capacity 。Buffer只能写capacity个byte、long、char等类型 。一旦Buffer满了 , 需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据 。
position
当写数据到Buffer中时 , position表示当前的位置 。初始的position值为0 。当一个byte、long等数据写到Buffer后 ,  position会向前移动到下一个可插入数据的Buffer单元 。position最大可为capacity – 1.
当读取数据时 , 也是从某个特定位置读 。当将Buffer从写模式切换到读模式 , position会被重置为0 。当从Buffer的position处读取数据时 , position向前移动到下一个可读的位置 。
资深架构师总结:彻底搞懂NIO效率高的原理

文章插图
 
limit
在写模式下 , Buffer的limit表示最多能往Buffer里写多少数据 。写模式下 , limit等于capacity 。
当切换Buffer到读模式时 ,  limit表示你最多能读到多少数据 。因此 , 当切换Buffer到读模式时 , limit会被设置成写模式下的position值 。
Selector
Selector允许单线程处理多个 Channel 。如果你的应用打开了多个连接(通道) , 但每个连接的流量都很低 , 使用Selector就会很方便 。例如 , 在一个聊天服务器中 。
这是在一个单线程中使用一个Selector处理3个Channel的图示:
资深架构师总结:彻底搞懂NIO效率高的原理

文章插图
 
要使用Selector , 得向Selector注册Channel , 然后调用它的select()方法 。这个方法会一直阻塞到某个注册的通道有事件就绪 。一旦这个方法返回 , 线程就可以处理这些事件 , 事件例如有新连接进来 , 数据接收等 。
NIO与epoll的关系
Java NIO根据操作系统不同 ,  针对NIO中的Selector有不同的实现:
  • macosx:KQueueSelectorProvider
  • solaris:DevPollSelectorProvider
  • linux:EPollSelectorProvider (Linux kernels >= 2.6)或PollSelectorProvider
  • windows:WindowsSelectorProvider
所以不需要特别指定 , Oracle JDK会自动选择合适的Selector 。如果想设置特定的Selector , 可以设置属性 , 例如:
-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider
JDK在Linux已经默认使用epoll方式 , 但是JDK的epoll采用的是水平触发 , 所以Netty自4.0.16起, Netty为Linux通过JNI的方式提供了native socket transport 。Netty重新实现了epoll机制 , 
  1. 采用边缘触发方式
  2. netty epoll transport暴露了更多的nio没有的配置参数 , 如 TCP_CORK, SO_REUSEADDR等等 。
  3. C代码 , 更少GC , 更少synchronized
使用native socket transport的方法很简单 , 只需将相应的类替换即可 。
NioEventLoopGroup → EpollEventLoopGroupNioEventLoop → EpollEventLoopNIOServerSocketChannel → EpollServerSocketChannelNioSocketChannel → EpollSocketChannelNIO处理消息的核心思路
结合示例代码 , 总结NIO的核心思路:
  1. NIO 模型中通常会有两个线程 , 每个线程绑定一个轮询器 selector  , 在上面例子中serverSelector负责轮询是否有新的连接 , clientSelector负责轮询连接是否有数据可读
  2. 服务端监测到新的连接之后 , 不再创建一个新的线程 , 而是直接将新连接绑定到clientSelector上 , 这样就不用BIO模型中1w 个while循环在阻塞 , 参见(1)
  3. clientSelector被一个 while 死循环包裹着 , 如果在某一时刻有多条连接有数据可读 , 那么通过clientSelector.select(1)方法可以轮询出来 , 进而批量处理 , 参见(2)


    推荐阅读