java NIO 的最佳实践

NIO的背景为什么一个已经存在10年的增强包还是JAVA的新I/O包呢?原因是对于大多数的Java程序员而言,基本的I/O操作都能够胜任 。在日常工作中,大部分的Java开发者没有必要去学习NIO 。更进一步,NIO不仅仅是一个性能提升包 。相反,它是一个和Java I/O相关的不同功能的集合 。NIO通过使得Java应用的性能“更加接近实质”来达到性能提升的效果,也就是意味着NIO和NIO.2的API暴露了低层次的系统操作的入口 。NIO的代价就是它在提供更强大的I/O控制能力的同时,也要求我们比使用基本的I/O编程更加细心地使用和练习 。NIO的另一特点是它对于应用程序的表现力的关注,这个我们会在下面的练习中看到 。
Java NIO和IO的主要区别

  1. 面向流与面向缓冲. Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的 。Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方 。此外,它不能前后移动流中的数据 。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区 。Java NIO的缓冲导向方法略有不同 。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动 。这就增加了处理过程中的灵活性 。
  2. 阻塞与非阻塞IO Java IO的各种流是阻塞的 。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入 。该线程在此期间不能再干任何事情了 。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,该线程可以继续做其他的事情 。非阻塞写也是如此 。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情 。线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel) 。
  3. 选择器(Selectors) Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道 。这种选择机制,使得一个单独的线程很容易来管理多个通道 。
最佳practiceSelectionKey.OP_WRITE订阅时机
现象: cpu占用超高
原因: 订阅了SelectionKey.OP_WRITE事件
【java NIO 的最佳实践】Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey selectionKey = iterator.next(); iterator.remove(); if (selectionKey.isConnectable()) { SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); if (socketChannel.isConnectionPending()) { socketChannel.finishConnect(); } socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); }分析: 当socket缓冲区可写入时就会触发OP_WRITE事件. 而socket缓冲区大多时间都可写入(网络不拥堵),由于nio水平触发的特性OP_WRITE会一直触发导致while()一直空转
水平触发: 简单解释为只要满足条件就一直触发,而不是发生状态改变时才触发(有点主动和被动触发的感觉)
最佳实践:
方案一: 当有写数据需求时订阅OP_WRITE事件,数据发送完成取消订阅.
while (channel.isOpen()) { if (channel.isConnected() && writeBuffer.isReadable()) { //writeBuffer可读 注册write事件 channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); }//当采用临时订阅OP_WRITE方式 必须使用select(ms)进行超时返回 // 因为很有可能当select()前极短时间内writeBuffer有数据,而此时没有订阅OP_WRITE事件,会使select()一直阻塞int ready = selector.select(300); if (ready > 0) {SelectionKey selectionKey = iterator.next(); iterator.remove(); SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); socketChannel.configureBlocking(false); if (selectionKey.isWritable()) {writeBuffer.flip(); while (writeBuffer.hasRemaining()) { channel.write(writeBuffer); } writeBuffer.clear();socketChannel.register(selector, SelectionKey.OP_READ); } } }当使用临时订阅OP_WRITE事件方式时,必须使用selector.select(long),进行超时返回. 因为很有可能当select()前极短时间内writeBuffer有数据,而此时没有订阅OP_WRITE事件,会使select()一直阻塞
方案二: 不订阅OP_WRITE事件,直接通过socketChannel.write()写数据.
Selector selector = Selector.open(); channel.register(selector, SelectionKey.OP_CONNECT); channel.connect(new InetSocketAddress("localhost", 5555)); while (channel.isOpen()) { if (channel.isConnected()) { writeBuffer.flip(); while (writeBuffer.hasRemaining()) { channel.write(writeBuffer); } writeBuffer.clear(); } int ready = selector.select(500); ...各种事件处理 }


推荐阅读