Java NIO的介绍
首先要搞清楚两个概念,一个是NIO
,另一个是NIO 2
。NIO = New I/O
是在JDK1.4
中引入,也就是同步非阻塞I/O
,简称NIO
;而NIO 2
是NIO
的升级版,在JDK1.7
中引入,也就是异步非阻塞I/O
,简称AIO
;而最早最传统的I/O
属于同步阻塞I/O
,简称BIO
。
Java-NIO的优势
- 事件驱动模型
- 避免多线程
- 单线程处理多任务
- 非阻塞
I/O
,I/O
读写不再阻塞,而是返回0 - 基于
block
的传输,通常比基于流的传输更高效 - 更高级的
I/O
函数,zero-copy
I/O
多路复用大大提高了Java网络应用的可伸缩性和实用性
NIO工作原理
- 由一个专门的线程来处理所有的
I/O
事件,并负责分发 - 事件驱动机制:事件到的时候触发,而不是同步的去监视事件
- 线程通讯:线程之间通过
wait
,notify
等方式通讯。保证每次上下文切换都是有意义的,减少无谓的线程切换
NIO开发相关模糊知识点
首先
Selector.open()
并不是单例模式,当你每次调用该静态方法时候,都返回一个全新的Selector
实例configureBlocking()
方法用来设置通道的阻塞模式,该方法会调用implConfigureBlocking
方法,implConfigureBlocking
方法会更改阻塞模式为新传入的值,如:默认为true,传入false,那么该通道将调整为非阻塞,可以通过调用isBlocking()
方法来判断某个socket
通道当前处于哪种模式NIO的最大优势就是其是非阻塞模型,所以一般来说都需要设置
SocketChannel.configureBlocking(false);
传统的
Java IO
中通道是阻塞的,那么NIO提供了非阻塞的通道到底有什么作用呢?非阻塞I/O
是许多复杂的、高性能的程序构建的基础remove()
方法的作用 ①Set selectedKeys = selector.selectedKeys(); Iterator iter = selectedKeys.iterator();
②SelectionKey key = iter.next();
③iter.remove();
注意每次迭代之后要移除当前迭代的对象,原因是Selector
不会自己从已选择键集中移除SelectionKey
实例。必须在处理完通道时自己移除。下次该通道变成就绪时,Selector
会再次将其放入已选择键集中服务端和客户端是否维护着同一份
Selector
,答案是否定的,服务端和客户端各自维护着一个Selector
对象,并且注意在多线程并发的时候,不要让多个客户端共享Selector
ByteBuffer byteBuffer = ByteBuffer.allocate(1);
在从通道往buffer
中读入之后,使用byteBuffer.get()
获取的时候,不可重复调用,因为get()
方法会移动position
,使得多次调用get()
方法获取的内容是不同的在
ByteBuffer
中,put(int index, byte b)
方法不会移动position
,但是put(byte b)
会移动position
channel.read()
函数会返回-1,那么什么时候会读到-1呢?针对服务器端而言,当客户端调用了channel.close()
关闭连接时,这时候服务器端返回的读取数是-1,表示已经到了末尾。那么此时需要把对应的SelectionKey
给cancel
掉,表示selector
不再监听这个channel
上的读事件,并且关闭channel
ByteBuffer.allocate(int capacity)
和ByteBuffer.allocateDirect(int capacity)
的区别:使用allocate
来创建缓冲区,并不是一下子就分配给缓冲区capacity
大小的空间,而是根据缓冲区中存储数据的情况来动态分配缓冲区的大小(实际上,在底层Java采用了数据结构中的堆来管理缓冲区的大小),因此,这个capacity
可以是一个很大的值,如1024*1024(1M)
。使用allocateDirect
方法可以一次性分配capacity
大小的连续字节空间。通过allocateDirect
方法来创建具有连续空间的ByteBuffer
对象虽然可以在一定程度上提高效率,但这种方式并不是平台独立的。也就是说,在一些操作系统平台上使用allocateDirect
方法来创建ByteBuffer
对象会使效率大幅度提高,而在另一些操作系统平台上,性能会表现得非常差。而且allocateDirect
方法需要较长的时间来分配内存空间,在释放空间时也较慢。因此,在使用allocateDirect
方法时应谨慎与缓冲区不同,通道不能被重复使用,一个打开的通道即代表与一个特定
I/O
服务的特定连接并封装该连接的状态。当通道关闭时,那个连接会丢失,然后通道将不再连接任何东西通道的
read()
和write()
方法的数据流向是怎么样的?read()
表示该通道读就绪,可以从通道中读取内容到缓冲区,而wirte()
表示该通道写就绪,可以将缓冲区中的内容写入通道虽然说一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次
选择键封装了特定的通道与特定的选择器的注册关系。选择键对象被
SelectableChannel.register(Selector sel, int ops)
返回并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数的形式进行编码),指示了该注册关系所关心的通道操作,以及通道已经准备好的操作注意
select()
是一个阻塞操作,那么如何停止select()
操作所在的线程呢?主要有三种方法 ①使用volatile boolean
变量来标识线程是否停止 ②停止线程时,需要调用停止线程的interrupt()
方法,因为线程有可能在wait()
或sleep()
,提高停止线程的及时性 ③处于Blocking IO
的处理,尽量使用InterruptibleChannel
来代替Blocking IO
,对于NIO来说,如果线程处于select()
阻塞状态,这时候无法及时的检测到条件变量的变化,那么需要人工调用wakeup()
方法,唤醒线程,使得其可以检测到条件变量当通道关闭时,所有相关的键会自动取消;当选择器关闭时,所有被注册到该选择器的通道都将被注销,并且相关的键将立即被无效化
注意
select()
操作返回值不是已经准备好的通道的总数,而是从上一个select()
调用之后进入就绪状态的通道的数量。之前的调用中就绪的,并且在本次调用中仍然就绪的通道不会被计入,而那些在前一次调用中已经就绪但已经不再处于就绪状态的通道也不会计入。这些通道可能仍然在已选择的键的集合中,但不会被计入返回值中,返回值可能是0推荐使用内部的已取消的键的集合来延迟注销,是一种防止线程在取消键时阻塞,并防止与正在进行的选择操作冲突的优化
Selector
选择器对象是线程安全的,但它们包含的键集合不是。通过keys()
和selectKeys()
返回的键的集合是Selector
对象内部的私有的Set
对象集合的直接引用。这些集合可能在任意时间被改变。已注册的键的集合是只读的如果在多个线程并发地访问一个选择器的键的集合的时候存在任何问题,可以采用同步的方式进行访问,在执行选择操作时,选择器在
Selector
对象上进行同步,然后是已注册的键的集合,最后是已选择的键的集合在并发量大的时候,使用同一个线程处理连接请求以及消息服务,可能会出现拒绝连接的情况,这是因为当该线程在处理消息服务的时候,可能会无法及时处理连接请求,从而导致超时;一个更好的策略是对所有的可选择通道使用一个选择器,并将对就绪通道的服务委托给其它线程。只需一个线程监控通道的就绪状态并使用一个协调好的的工作线程池来处理接收及发送数据
Selector.wakeup()
的主要作用:①解除阻塞在Selector.select()/select(long)
上的线程,立即返回 ②两次成功的select
之间多次调用wakeup
等价于一次调用 ③如果当前没有阻塞在select
上,则本次wakeup
调用将作用于下一次select
操作