为什么SelectionKey集合需要迭代删除--从selector说起
为什么SelectionKey集合需要迭代删除–从selector说起
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel =ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(8080);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
int nums = selector.select();
System.out.println("get count" + nums);
Set<SelectionKey> keySet = selector.selectedKeys();
Iterator<SelectionKey> iterator = keySet.iterator();
while (iterator.hasNext()) {
SelectionKey next = iterator.next();
if (next.isAcceptable()) {
//dosomthing
iterator.remove();
}else if(next.isReadable()){
SocketChannel channel = (SocketChannel) next.channel();
//dosomthing
iterator.remove();
}
}
}
上面的代码是一个很典型的Java NIO服务器端的编写方式,有一个问题为什么 Set<SelectionKey> keySet
需要每次迭代的进行删除.这里就来一起探讨一下为什么一定需要这么作.
首先我们看看注册时 Channel 做了什么
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
这段代码到底做了什么事情呢?
java.nio.channels.SelectableChannel
public final SelectionKey register(Selector sel, int ops)
throws ClosedChannelException{
return register(sel, ops, null);
}
接着进入被调用方法看看
java.nio.channels.SelectableChannel
public abstract SelectionKey register(Selector sel, int ops, Object att)
throws ClosedChannelException;
这是个抽象方法,只能查看具体的子类是如何实现相关的逻辑的
java.nio.channels.spi.AbstractSelectableChannel
public final SelectionKey register(Selector sel, int ops, Object att)
throws ClosedChannelException
{
if ((ops & ~validOps()) != 0)
//判断注册事件是否合理
throw new IllegalArgumentException();
if (!isOpen())
//判断通道状态是否正确
throw new ClosedChannelException();
synchronized (regLock) {
//注册时进行同步操作
if (isBlocking())
throw new IllegalBlockingModeException();
synchronized (keyLock) {
// re-check if channel has been closed
if (!isOpen())
throw new ClosedChannelException();
SelectionKey k = findKey(sel);//查询该通道所相关连的选择器
//找到了key就绑定一个对象(猜测一下应该是上下文对象)以及一个感兴趣的事件
if (k != null) {
k.attach(att);
k.interestOps(ops);
} else {//没有找到关联key,只能让选择器来注册,应该是第一次进行注册
// New registration
k = ((AbstractSelector)sel).register(this, ops, att);
addKey(k); //蛮猜测一下就是把 k 添加到对应的数组中去
}
return k;
}
}
}
我们来看看findKey(sel)
到底在做什么(这个明显是成员方法,内部调用)
private SelectionKey findKey(Selector sel) {
assert Thread.holdsLock(keyLock);
// private SelectionKey[] keys = null;
// keys是一个成员变量
if (keys == null)
return null;
// 说白了就是遍历查询 keys 这个数组对象找到与之相对应的 SelectionKey 对象
// 因此可以推测出来Channel是可以注册多个selector并且可以重复注册 这就是直接证据
for (int i = 0; i < keys.length; i++)
if ((keys[i] != null) && (keys[i].selector() == sel))
return keys[i];
return null;
}
addKey
是否如上文的判断看一下代码
private void addKey(SelectionKey k) {
assert Thread.holdsLock(keyLock); // 判断是否持有锁?
int i = 0;
// 数组不为null 找到keys 为null的位置 赋值给 i
// keycount 为持有的key数量
if ((keys != null) && (keyCount < keys.length)) {
// Find empty element of key array
for (i = 0; i < keys.length; i++)
if (keys[i] == null)
break;
} else if (keys == null) { // 数组为 null 创建数组
keys = new SelectionKey[2];
} else {
// Grow key array
// 数组扩容 为什么不使用 system.arrayCopy ?
int n = keys.length * 2;
SelectionKey[] ks = new SelectionKey[n];
for (i = 0; i < keys.length; i++)
ks[i] = keys[i];
keys = ks;
i = keyCount;
}
// 赋值 & keyCount 持有key数量++
keys[i] = k;
keyCount++;
}
java.nio.channels.spi.AbstractSelector
protected abstract SelectionKey register(AbstractSelectableChannel ch,
int ops, Object att);
抽象方法,看看他的子类是如何实现的
sun.nio.ch.SelectorImpl
protected final SelectionKey register(AbstractSelectableChannel ch,
int ops,
Object attachment){
if (!(ch instanceof SelChImpl))
throw new IllegalSelectorException();
// 创建一个 SelectionKey 的实例
SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
// 注册一个关联对象
k.attach(attachment);
// 根据代码跟踪就是检查对象状态
// register (if needed) before adding to key set
implRegister(k);
// add to the selector's key set, removing it immediately if the selector
// is closed. The key is not in the channel's key set at this point but
// it may be observed by a thread iterating over the selector's key set.
// private final Set<SelectionKey> keys;
// keys 本身就是一个 SelectionKey 集合
keys.add(k);
try {
// 关联感兴趣的操作
k.interestOps(ops);
} catch (ClosedSelectorException e) {
// 出现异常 取消注册, 将keys中的这个对象移除
assert ch.keyFor(this) == null;
keys.remove(k);
k.cancel();
throw e;
}
// 最终返回一个 SelectionKey 实例
return k;
}
综上所述,可以看出
第一次进行注册时会同时往 selector Channel 对象中添加一个 SelectionKey 的实例.
- 创建一个 SelectionKey 的实例 key
- key 关联对象 key 关联感兴趣操作
- selector 对象 内部的 key 集合添加元素
- Channel 对象 内部 key 数组添加元素
如果不是第一次注册
- 找到 selector 对应的 key
- key 关联 对象,感兴趣的操作
我们来看看 selector 在 select() 都在搞什么
java.nio.channels.Selector
public abstract Set<SelectionKey> selectedKeys();
明显这里是一个抽象方法,我们来看看他的子类是怎么实现的
sun.nio.ch.SelectorImpl
@Override
public final Set<SelectionKey> selectedKeys() {
ensureOpen();
// private final Set<SelectionKey> publicSelectedKeys;
// 这个是一个 SelectionKey 的集合 直接就返回了
return publicSelectedKeys;
}
从上面很简单的两段代码看出只是返回了一个集合而已,所以可以大胆猜测一定有什么机制使得
publicSelectedKeys 这个属性这个集合里面的元素进行修改.
结合一下我们需要在迭代器中删除元素可以推测应该是只能往这里面添加元素而不能进行删除操作,所以只能由调用用户进行逻辑处理完成之后删除.
跟踪代码 publicSelectedKeys 在哪里添加元素
sun.nio.ch.SelectorImpl
protected SelectorImpl(SelectorProvider sp) {
super(sp);
keys = ConcurrentHashMap.newKeySet();
// private final Set<SelectionKey> selectedKeys;
selectedKeys = new HashSet<>();
publicKeys = Collections.unmodifiableSet(keys);
// private final Set<SelectionKey> publicSelectedKeys;
// Removal allowed, but not addition
publicSelectedKeys = Util.ungrowableSet(selectedKeys);
}
publicSelectedKeys 被赋值了一次
Util.ungrowableSet
static <E> Set<E> ungrowableSet(final Set<E> s) {
return new Set<E>() {
public int size() { return s.size(); }
public boolean isEmpty() { return s.isEmpty(); }
public boolean contains(Object o) { return s.contains(o); }
public Object[] toArray() { return s.toArray(); }
public <T> T[] toArray(T[] a) { return s.toArray(a); }
public String toString() { return s.toString(); }
public Iterator<E> iterator() { return s.iterator(); }
public boolean equals(Object o) { return s.equals(o); }
public int hashCode() { return s.hashCode(); }
public void clear() { s.clear(); }
public boolean remove(Object o) { return s.remove(o); }
public boolean containsAll(Collection<?> coll) {
return s.containsAll(coll);
}
public boolean removeAll(Collection<?> coll) {
return s.removeAll(coll);
}
public boolean retainAll(Collection<?> coll) {
return s.retainAll(coll);
}
public boolean add(E o){
throw new UnsupportedOperationException();
}
public boolean addAll(Collection<? extends E> coll) {
throw new UnsupportedOperationException();
}
};
}
可以看出 publicSelectedKeys 只是包装了一下而已,做了一个不允许添加元素的限制,本质上都还是selectedKeys 在处理. 另外从名称也可以看出,publicSelectedKeys 是暴露给外部的集合, 但是selectedKeys 是已经被选择的集合,这里应该是防止调用方会搞事直接包装了一下不允许外部调用方添加元素. 说白了就是你用完可以删除,但是不能乱加,应该是希望只能由操作系统读取到了连接触发之后才能添加元素.
selectedKeys 在哪里添元素
sun.nio.ch.SelectorImpl
/**
* Invoked by selection operations to handle ready events. If an action
* is specified then it is invoked to handle the key, otherwise the key
* is added to the selected-key set (or updated when it is already in the
* set).
*/
/**
*由选择操作调用以处理就绪事件。 如果一个动作
*被指定然后调用它来处理**,否则键
*被添加到选定的**集(或当它已经在的时候更新)
*设置)。
*/
protected final int processReadyEvents(int rOps,
SelectionKeyImpl ski,
Consumer<SelectionKey> action) {
if (action != null) {
ski.translateAndSetReadyOps(rOps);
if ((ski.nioReadyOps() & ski.nioInterestOps()) != 0) {
action.accept(ski);
ensureOpen();
return 1;
}
} else {
assert Thread.holdsLock(publicSelectedKeys);
if (selectedKeys.contains(ski)) {
if (ski.translateAndUpdateReadyOps(rOps)) {
return 1;
}
} else {
ski.translateAndSetReadyOps(rOps);
if ((ski.nioReadyOps() & ski.nioInterestOps()) != 0) {
// 看这里
selectedKeys.add(ski);
return 1;
}
}
}
return 0;
}
sun.nio.ch.SelectorImpl#processReadyEvents()
在哪里被调用了呢
sun.nio.ch.EPollSelectorImpl
(应该是linux的实现)
class EPollSelectorImpl extends SelectorImpl
private int processEvents(int numEntries, Consumer<SelectionKey> action)
throws IOException {
assert Thread.holdsLock(this);
boolean interrupted = false;
int numKeysUpdated = 0;
for (int i=0; i<numEntries; i++) {
long event = EPoll.getEvent(pollArrayAddress, i);
int fd = EPoll.getDescriptor(event);
if (fd == fd0) {
interrupted = true;
} else {
SelectionKeyImpl ski = fdToKey.get(fd);
if (ski != null) {
int rOps = EPoll.getEvents(event);
numKeysUpdated += processReadyEvents(rOps, ski, action);
}
}
}
if (interrupted) {
clearInterrupt();
}
return numKeysUpdated;
}
@Override
protected int doSelect(Consumer<SelectionKey> action, long timeout)
throws IOException{
assert Thread.holdsLock(this);
// epoll_wait timeout is int
int to = (int) Math.min(timeout, Integer.MAX_VALUE);
boolean blocking = (to != 0);
boolean timedPoll = (to > 0);
int numEntries;
processUpdateQueue();
processDeregisterQueue();
try {
begin(blocking);
do {
long startTime = timedPoll ? System.nanoTime() : 0;
numEntries = EPoll.wait(epfd, pollArrayAddress, NUM_EPOLLEVENTS, to);
if (numEntries == IOStatus.INTERRUPTED && timedPoll) {
// timed poll interrupted so need to adjust timeout
long adjust = System.nanoTime() - startTime;
to -= TimeUnit.MILLISECONDS.convert(adjust, TimeUnit.NANOSECONDS);
if (to <= 0) {
// timeout expired so no retry
numEntries = 0;
}
}
} while (numEntries == IOStatus.INTERRUPTED);
assert IOStatus.check(numEntries);
} finally {
end(blocking);
}
processDeregisterQueue();
return processEvents(numEntries, action);
}
sun.nio.ch.SelectorImpl
private int lockAndDoSelect(Consumer<SelectionKey> action, long timeout)
throws IOException
{
synchronized (this) {
ensureOpen();
if (inSelect)
throw new IllegalStateException("select in progress");
inSelect = true;
try {
synchronized (publicSelectedKeys) {
return doSelect(action, timeout);
}
} finally {
inSelect = false;
}
}
}
@Override
public final int select() throws IOException {
return lockAndDoSelect(null, -1);
}
所以本文第一段代码就是这样写的
int nums = selector.select();
System.out.println("get count" + nums);
Set<SelectionKey> keySet = selector.selectedKeys();
先查询了一个有多少个,然后再去查询出来相应的key集合
篇文章其实写的也不太好,但是看完之后应该对NIO
和epoll
模型算是一个其他角度的理解了,调用线程不用一直阻塞的监听每个Channel的状态更改,直接使用select()方法去让选择器找一遍关联的Channel看看那些有更新,然后将更新的Channel以SelectionKey的形式添加到selectedKeys
这个集合当中,外部通过调用selectedKeys()
方法返回了一个不允许被修改的key集合publicSelectedKeys
.并且因为在实现的代码内部是不知道key在什么时候 被消费的,所以一直没有对其进行删除,所以外部需要在迭代器内部进行迭代处理完成后remove()
否则就会出现一直有这个ke存在
最后想说声抱歉,因为能力/时间有限,尚未对最后几段调用链上的代码作代码注释分析,后续有机会补上.
上一篇: Socket
下一篇: 07-NIO SelectionKey