Selector(选择器)是Java NIO中能够同时监测多个Channel通道,并且还能知道Channel上读写事件是否准备好。这样一个Selector线程就可以管理多个Channel,而不像Blocking IO那样一个线程对应一个监管一个IO事件。
Selector特点
一个Selector对应多个Channel:仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好。
select、poll、epoll模式
select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
- select模式
特点:忙轮询,文件描述符有大小限制,会将整个文件描述符数组频繁在用户内存空间和内核的内存地址空间的拷贝复制,造成较大的性能消耗。
过程:不停地从头到尾遍历整个文件描述符,根据每一个描述符的状态进行通知处理,被通知的线程还需要遍历整个文件描述符来判断是哪个事件准备好了。
- poll模式
特点:轮询,没有文件描述符大小限制(有系统内存大小相关),但是同样会将整个文件描述符数组频繁在用户内存空间和内核的内存地址空间的拷贝复制,造成较大的性能消耗
过程:为了避免CPU空转,可以同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,被唤醒的程序就会轮询一遍所有的文件描述符,来判断哪个IO的事件就绪,并进行相应的处理。
- epoll模式
特点:基于IO事件响应,高性能,没有文件描述符大小限制,只会将准备就绪的IO描述符进行复制。
过程:epoll同时观察许多流的I/O事件,在没有IO事件发生时,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,被唤醒的程序就能获取所有已经准备就绪的文件描述符,并进行相应的处理。
- epoll模式的其他特性
- 在Linux2.6(包括)之后使用epoll模式实现Java NIO,在用户程序使用上是没有差别的,只是遍历Selector.selectedKeys()方法返回集合的时间复杂度为O(n)或O(1),其中n为整个文件描述符的数量。
- epoll模式的高效性:epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的socket,这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象,避免了内核内存空间和用户内存空间的复制消耗。
Selector的使用
Selector总是和Channel成对出现的,与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。
从一个完整的程序示例开始
需要留意的步骤:
步骤3
使用Selector选择器需要将Channel设置为非阻塞状态,FileChannel不可设置为非阻塞状态,套接字可以。
步骤5
注意通常不会注册写就绪事件,因为在发送缓冲区未满的情况下始终是可写的,而且
注册写事件,而又不用写数据,则缓冲区未满总会响应写事件就绪,很容易造成CPU空转,出现消耗CPU100%的情况。
注册的事件可以为:
- SelectionKey.OP_CONNECT(某个channel成功连接到另一个服务器称为“连接就绪”)
- SelectionKey.OP_ACCEPT(channel准备好接收新进入的连接称为“接收就绪”,对应于ServerSocket.accept方法)
- SelectionKey.OP_READ(一个有数据可读的通道可以说是“读就绪”)
- SelectionKey.OP_WRITE(等待写数据的通道可以说是“写就绪”)
register()方法会返回一个SelectionKey对象
- interest集合(所选择的感兴趣的事件集合)
- ready集合(通道已经准备就绪的操作的集合)
这是一个复合int类型字段,通过如下方法可以进行判断
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable()
- Channel、Selector
通过如下方法获取触发此IO事件的Channel,以及被哪个Selector监听到的
- 附加的对象(可选,是在注册Channel时一同注册的一个对象)
步骤6.1
通过select方法获取通道
select()方法返回的int值表示有多少通道已经就绪。亦即,自上次调用select()方法后有多少通道变成就绪状态。如果调用select()方法,因为有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。
- int select()
select()阻塞到至少有一个通道在你注册的事件上就绪了
- int select(long timeout)
select(long timeout)和select()一样,除了最长会阻塞timeout毫秒(参数)
- int selectNow()
selectNow()不会阻塞,不管什么通道就绪都立刻返回
如何唤醒阻塞在select方法的线程
- Selector.wakeup()
将使得选择器上的第一个有就绪Channel但是还没有返回的选择操作立即返回。如果当前没有就绪Channel被选择,那么下一次对 select( )方法的一种形式的调用将立即返回,后续的选择操作将正常进行。在选择操作之间多次调用 wakeup( )方法与调用它一次没有什么不同。有时这种延迟的唤醒行为并不是您想要的,您可以通过在调用 wakeup( )方法后调用 selectNow( )方法来绕过这个问题,此时需要将代码合理地关注于返回值和执行选择集合。
- Selector.close( )
如果选择器的 close( )方法被调用,那么任何一个在选择操作中阻塞的线程都将被唤醒,与选择器相关的通道将被注销,而键将被取消。通道本身并不会关闭。
- Thread.interrupt( )
抛出中断异常,程序异常返回,如果捕获异常则进行清理操作。
步骤6.3
每次迭代末尾的keyIterator.remove()调用。
Selector不会自己从已选择键集中移除SelectionKey实例。必须在处理完通道时自己移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中。实际上在两次调用select( )方法之间,都必须手动将其清空,换句话说,select( )方法只会在已有的所选键集上添加键,它们不会创建新的建集。
步骤7
先使Selector的注册信息失效,然后在关闭Channel
先关闭Selector,然后在关闭Channel,平滑的关闭整个系统。