NIO基础入门(六)之选择器selectors

1.概念

使单线程能够有效的管理多个I/O通道,非阻塞的方式节省资源消耗

主要由三个有关类组成

2.组成

  1. Selector选择器
  2. SelectableChannel 可选择通道
  3. SelectionKey 选择键

注意:选择器才是管理功能的对象

1.Selector选择器

选择器类管理着一个被注册的通道集合的信息和就绪状态

通道和选择器一起被注册,并通过选择器来更新通道状态

方法

open()

建立选择器对象

isOpen()

判断选择器对象是否建立

close()

provider()

select()

可立刻或指定睡眠时间后准备好I/O,进行调用

selectNow()

wakeup()

停止选择的过程(退出select睡眠)

keys()

selectedKeys()

2.SelectableChannel可选择通道

这个抽象类提供了实现通道可选择性所需要的公共方法

所有的socket和Pipe都是可选择的,但是FileChannel不是可选择的(没有继承SelectableChannel父类)

  • SelectableChannel可以被注册到选择器,并且可以指定哪个选择器
  • 一个通道可以被注册到多个选择器(后续注册的通道策略更新之前的策略),但一个选择器只能注册一次

方法

register()

注册通道到这顶选择器

isRegistered()

通道是否被注册

keyFor()

返回该通道选择器之间关系的选择键对象(没有注册关系则返回null)

configureBlocking()

isBlocking()

blockingLock()

以上三个为非阻塞相关方法

调用register()方法将可选择通道注册到选择器

只能注册非阻塞通道,否则异常;而且该通道无法改回阻塞通道

试图注册已关闭的通道也会抛出异常

3.SelectionKey选择键

选择键封装了特定的通道特定的选择器之间的注册关系(和操作策略,读/写/连接/接收等)

validOps()方法可以获得指定通道支持哪些操作策略

选择器对象被SelectableChannel.register()返回并提供一个表示注册关系的标记

标记:该注册关系关心的通道操作通道已经准备好的操作

方法

(可以提前通过keyFor获得选择键对象)

(可通过通道注册选择器返回该对象)

channel()

返回与该键相关的通道对象

selector()

返回与该键相关的选择器对象

cancel()

终结通道和选择器的注册关系(注册不会立刻消失,但键会立刻失效)

isValid()

判断通道和选择器是否有注册关系

interestOps()

interest策略集合,如果需要这些操作策略,从该方法中获得

选择器下次调用select()时生效

readyOps()

ready策略集合,如果需要这些操作策略,从该方法中获得

当前select()操作即可生效

if((key.readyOps()&SelectionKeyOP_READ)!=0)

isReadable()

是否准备好读

isWritable()

是否准备好写

isConnectable()

是否准备好连接

isAcceptable()

是否准备好接收

attach()

保存一个任意对象作为附件,选择键除了保存他,不会再其他地方调用,可以存放业务对象/会话句柄/其他通道等等

attachment()

通过该方法获得附件,如果没有附件,则返回null

3.选择器操作

1.建立选择器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SocketChannel channel1 = SocketChannel.open();
channel1.bind(new InetSocketAddress("localhost", 8001));
SocketChannel channel2 = SocketChannel.open();
channel2.bind(new InetSocketAddress("localhost", 8002));
SocketChannel channel3 = SocketChannel.open();
channel3.bind(new InetSocketAddress("localhost", 8003));

channel1.configureBlocking(false);
channel2.configureBlocking(false);
channel3.configureBlocking(false);

Selector selector = Selector.open();
channel1.register(selector, SelectionKey.OP_READ);
channel2.register(selector,SelectionKey.OP_READ|SelectionKey.OP_WRITE);
channel3.register(selector,SelectionKey.OP_READ|SelectionKey.OP_WRITE);
selector.select(10000);

2.关闭选择器

1
2
3
4
if (selector.isOpen()){
selector.close();
System.out.println("选择器关闭成功");
}

3.选择器维护三种键

  1. 已注册键的集合Registered key set,通过keys()方法返回,可能是空
  2. 已选择键的集合(注册建集合的子类)Selected key set,这些键包还在interset集合的操作,selectedKeys()返回,可能是空
  3. 已取消键的集合Cancelled key set,包含了cancel()方法被调用过得键,还没有被注销,但已无效(下次select时注销)

4.停止选择过程

有三种方法停止

  1. Selector中的wakeup(),使线程从被阻塞(睡眠)的select()方法中优雅的退出;
  2. Selector中的close(),阻塞线程被唤醒,但是与选择器相关的通道会被注销,键也会被取消
  3. Selector中的interrupt(),中断,如果继续执行会抛出异常

以上三个方法只会中断选择器,并不会中断通道

5.异步关闭能力

当一个通道被关闭,选择器的select()并不会受影响,这意味着调用select()之前仍然有效,但是返回时无效

6.选择过程的可扩展性(重点)

相对于使用一个线程来管理所有通道而言,在分布式不同业务环境下可能不是最好的选择

一个更好的策略是(负载):

对所有可选择通道使用一个选择器,然后对就绪通道的服务委托给其他线程.

  1. 这样可以通过一个线程来监控通道的就绪状态
  2. 通过一个协调好的线程池来处理接受到的数据

另一个场景:

某些通道要求比其他通道更高的响应速度

可以使用两个选择器管理,

  1. 一个为命令连接服务
  2. 一个为普通连接服务

这样类似于第一种策略,但分配更为精细,会根据情况使用多个线程池来为准备好的通道提供服务,

如:日志线程池,命令/控制线程池,状态请求线程池等等