这篇文章主要介绍NIO三大组件之一selector,看看selector是如何管理多个 channel,获取这些 channel 上发生的事件的
在使用nio编程时通常使用如下几句代码让selector生效:
ServerSocketChannel ssc = ServerSocketChannel.open();
//1.创建selector
Selector selector = Selector.open();
//2.注册selector要管理的channel及关心的事件
SelectionKey ssck = ssc.register(selector, SelectionKey.OP_ACCEPT, null);
//3.阻塞监听发生在channel上关心的事件
selector.select();
//4.获取监听到的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
//nio处理完事件,不会将key从集合中移除,所以需要手动移除,否则selector.select()会一直有事件
iterator.remove();
一. Selector.open()
入口:Selector.open() ->SelectorProvider.provider()->WindowsSelectorProvider.openSelector()
package sun.nio.ch;
public class WindowsSelectorProvider extends SelectorProviderImpl {
public AbstractSelector openSelector() throws IOException {
return new WindowsSelectorImpl(this);
}
}
WindowsSelectorImpl(SelectorProvider sp) throws IOException {
//1.创建selectorImpl父类
super(sp);
//2.创建PollArrayWrapper对象,创建对象时会分配一块推外虚拟内存,用来存放感兴趣事件掩码和文件描述符,通常是:当向selector注册channel及对应感兴趣事件时,将其对应socket的文件描述符和感兴趣掩码存入到pollArray
pollWrapper = new PollArrayWrapper(INIT_CAP);
//3.创建nio的管道,并保存该管道的source和sink通道的文件描述符,后期用于唤醒selector
wakeupPipe = Pipe.open();
wakeupSourceFd = ((SelChImpl)wakeupPipe.source()).getFDVal();
SinkChannelImpl sink = (SinkChannelImpl)wakeupPipe.sink();
//3.1 sink通道禁用Nagle算法,使唤醒更加即时(禁用Nagle算法,当sink端写入1字节数据时,将立即发送,而不必等到将较小的包组合成较大的包再发送,这样source端就可以立马读取数据)
(sink.sc).socket().setTcpNoDelay(true);
wakeupSinkFd = ((SelChImpl)sink).getFDVal();
//4.将source通道的文件描述符和感兴趣Net.POLLIN读事件保存到pollWrapper中
//index=0,说明wakeupSourceFd是第一个被放到到pollWrapper中的,后期用于唤醒selector
pollWrapper.addWakeupSocket(wakeupSourceFd, 0);
}
package sun.nio.ch;
private final int INIT_CAP = 8;
//注册到selector上的SelectionKey
private final Set<SelectionKey> keys;
// 已准备就绪的selectionKey,即可以被selector.select()获取到的selectionKey
private final Set<SelectionKey> selectedKeys;
// 将keys包装成不可修改的set,即 既不能添加也不能删除
private final Set<SelectionKey> publicKeys; // Immutable
//将selectedKeys包装成只可移除不能添加的set
private final Set<SelectionKey> publicSelectedKeys;
//创建selectorImpl,初始化变量
protected SelectorImpl(SelectorProvider sp) {
super(sp);
//1.存放channel注册到selector上产生的的selectionKey集合
keys = ConcurrentHashMap.newKeySet();
//2.存放就绪的selectionKey
selectedKeys = new HashSet<>();
//3.将keys包装成不可修改的set
publicKeys = Collections.unmodifiableSet(keys);
//4.对selectedKeys简单封装,里面封装的方法都是间接调用selectedKeys的方法,比如迭代,判空,但是不能add
publicSelectedKeys = Util.ungrowableSet(selectedKeys);
}
package sun.nio.ch;
static short SIZE_POLLFD = 8
@Native private static final short FD_OFFSET = 0; // fd offset in pollfd
@Native private static final short EVENT_OFFSET = 4; // events offset in pollfd
// 创建的allocatedNativeObject对象
private AllocatedNativeObject pollArray;
//pollArray对象持有的分配的推外虚拟内存的地址
long pollArrayAddress;
PollArrayWrapper(int newSize) {
//1.内存大小,默认8*8
int allocationSize = newSize * SIZE_POLLFD;
//2.创建AllocatedNativeObject对象,该类继承NativeObject,
//创建nativeObject主要利用unsafe.allocateMemory(size + ps)分配了一块推外虚拟内存
//内存大下为:allocationSize+系统默认一页的大小
//要想释放这部分内存,需要调用freeMemory或者reallocateMemory方法
pollArray = new AllocatedNativeObject(allocationSize, true);
//3.上面分配的内存地址
pollArrayAddress = pollArray.address();
this.size = newSize;
}
//创建对象时分配的内存主要用来存放下面提到的文件描述符和事件掩码,通常是:当向selector注册channel及对应感兴趣事件时,将其对应socket的文件描述符和感兴趣掩码调用下面的方法将其存入到pollArray,
void putDescriptor(int i, int fd) {
pollArray.putInt(SIZE_POLLFD * i + FD_OFFSET, fd);
}
void putEventOps(int i, int event) {
pollArray.putShort(SIZE_POLLFD * i + EVENT_OFFSET, (short)event);
}
总结
-
创建WindowsSelectorImpl对象时,创建了Pipe管道作为wakeupPipe,并保存pipe的sink和source通道文件描述符,并将source通道的文件秒数据和感兴趣事件Net.POLLIN存入到pollWrapper中,后面用于selector的唤醒
-
Pipe作用:pipe是通过两个连接的socket组成,sink和source,当sink有数据写入时,就可以从source中读取往sink写入的数据了
-
wakeupPipe主要用于唤醒seletor.select()所在的线程,唤醒原理参见:https://www.cnblogs.com/yungyu16/p/13065194.html
二.channel.register(Selector sel, int ops, Object att)
package sun.nio.ch;
abstract class SelectorImpl extends AbstractSelector {
//注册到selector上的selectionKey
private final Set<SelectionKey> keys;
@Override
protected final SelectionKey register(AbstractSelectableChannel ch,
int ops,
Object attachment)
{
if (!(ch instanceof SelChImpl))
throw new IllegalSelectorException();
//1.创建selectionKey,持有selector和channel信息
SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
//2.设置附件
k.attach(attachment);
implRegister(k);
//3.将selectionKey添加到keys集合
keys.add(k);
try {
//4.设置感兴趣事件
k.interestOps(ops);
} catch (ClosedSelectorException e) {
assert ch.keyFor(this) == null;
keys.remove(k);
k.cancel();
throw e;
}
return k;
}
总结
- 创建selectionKey,selectionKey可以看成是channel , 事件,selector的映射
- register主要就是将创建的selectionKey放入SelectorImpl的keys集合中,供后面的
selector.select()
使用
三. selector.select()
package sun.nio.ch;
abstract class SelectorImpl extends AbstractSelector{
//注册到selector上的selectionKey,及调用channel.register时产生的slectionKey
private final Set<SelectionKey> keys;
//保存被取消的selectionKey,调用selectionKey.cannel()时selectionkey就会被加入cancelledKeys
private final Set<SelectionKey> cancelledKeys = new HashSet<SelectionKey>();
//保存就绪的selectionKey,即有事件发生的selectionKey(主线程和辅助线程都完成poll后在updateSelectedKeys(Consumer<SelectionKey> action)被放入)
private final Set<SelectionKey> selectedKeys;
@Override
public final int select() throws IOException {
//默认timeout=-1,阻塞执行
return lockAndDoSelect(null, -1);
}
private int lockAndDoSelect(Consumer<SelectionKey> action, long timeout)
throws IOException
{
synchronized (this) {
//1.确保selector是open状态,即selectorOpen变量为true,调用selector.close()会设置为false,初始值为true
ensureOpen();
//2.inSelect保证只有一个线程在执行selector.selector()
if (inSelect)
throw new IllegalStateException("select in progress");
inSelect = true;
try {
synchronized (publicSelectedKeys) {
//3.真正阻塞执行select
return doSelect(action, timeout);
}
} finally {
//4.doSelect 无论是否正确执行,都要将inSelect设为fasle,保证其它下一次的select正常执行
inSelect = false;
}
}
}
//处理被取消的selectionKey,调用selectionKey.cannel()时selectionkey就会被加入cancelledKeys
protected final void processDeregisterQueue() throws IOException {
assert Thread.holdsLock(this);
assert Thread.holdsLock(publicSelectedKeys);
Set<SelectionKey> cks = cancelledKeys();
synchronized (cks) {
if (!cks.isEmpty()) {
Iterator<SelectionKey> i = cks.iterator();
while (i.hasNext()) {
SelectionKeyImpl ski = (SelectionKeyImpl)i.next();
i.remove();
//1.用pollArray中最后一个selectionKey相关信息替换要取消的selectionkey
implDereg(ski);
//2.从有事件发生的selectionKey集合中移除
selectedKeys.remove(ski);
//3.从注册到selector上的selectionKey集合中移除
keys.remove(ski);
//4. remove from channel's key set
deregister(ski);
//5.channel为关闭且为注册,则kill
SelectableChannel ch = ski.channel();
if (!ch.isOpen() && !ch.isRegistered())
((SelChImpl)ch).kill();
}
}
}
}
}
WindowsSelectorImpl 里面的代码很重要了,实现了select如何poll的具体实现
package sun.nio.ch;
//channelArray 初始容量
private final int INIT_CAP = 8;
//保存channel绑定到selector时产生的selectionKey
private final Deque<SelectionKeyImpl> newKeys = new ArrayDeque<>();
//保存感兴趣事件发生变化的selectionKey
private final Deque<SelectionKeyImpl> updateKeys = new ArrayDeque<>();
//保存每次需要被select的selecttionkey,即每次select时 拉取到的注册到seletor上的selectionKey的有效selectionKey,下次select会覆盖掉上次的selectionKey
private SelectionKeyImpl[] channelArray = new SelectionKeyImpl[INIT_CAP];
//保存每次需要被select的channel数,即注册到selector上的有效channel数量,初始值为1即wakeupChannel
private int totalChannels = 1;
//保存文件描述符和SelectionKey的映射关系
private final FdMap fdMap = new FdMap();
//保存 注册到selector上的channel对应的文件描述符及感兴趣事件
private PollArrayWrapper pollWrapper;
//标识是否执行唤醒,即向wakeupSink中写入数据
private volatile boolean interruptTriggered;
//辅助线程数
private int threadsCount = 0;
//辅助线程集合
private final List<SelectThread> threads = new ArrayList<SelectThread>();
class WindowsSelectorImpl extends SelectorImpl {
@Override
protected int doSelect(Consumer<SelectionKey> action, long timeout)
throws IOException
{
assert Thread.holdsLock(this);
//1.保存超时时间
this.timeout = timeout; // set selector timeout
//2.处理将被要select的selectionKey,将其文件描述符和事件保存到pollWrapper
processUpdateQueue();
//2.处理被取消的selectionKey
processDeregisterQueue();
//3.如果是中断标志,调用本地方法resetWakeupSocket0读取wakeupSink向wakeupSource发送的数据,并将interruptTriggered设置为false,方法直接返回,不再执行真正的poll
if (interruptTriggered) {
resetWakeupSocket();
return 0;
}
//4. 判断辅助线程数(守护线程),少则添加多则移除,添加的同时并start(),即selectThread.start()
adjustThreadsCount();
//5.重置FinishLock的threadsToFinish数为辅助线程数
finishLock.reset(); // reset finishLock
//6.唤醒所有的辅助线程,即所有辅助线程开始等待分配的selectionKey有事件发生
startLock.startThreads();
try {
//7.设置主线程中断的回调函数,从这开始进行poll拉取事件了,即轮训各组负责的部分pollWrapper中的FD
begin();
try {
//8. 主线程开始poll,阻塞等待有事件发生
subSelector.poll();
} catch (IOException e) {
finishLock.setException(e); // Save this exception
}
//9. 有辅助线程执行,主线程执行完,唤醒并等待所有未执行完的辅助线程完成
if (threads.size() > 0)
finishLock.waitForHelperThreads();
} finally {
end();
}
// Done with poll(). Set wakeupSocket to nonsignaled for the next run.
finishLock.checkForException();
processDeregisterQueue();
//10.获取有感兴趣事件发生的selectionKey数量
int updated = updateSelectedKeys(action);
//11.本轮poll完成,重置wakeupSocket
resetWakeupSocket();
return updated;
}
}
//将有效的,将要被select的selectionKey的保存 并将其 文件描述符和事件保存到pollWrapper
private void processUpdateQueue() {
assert Thread.holdsLock(this);
synchronized (updateLock) {
SelectionKeyImpl ski;
//1. 队列中有元素,说明有channel注册到selector上(调用channel.register时会将生成的selectionKey保存到newKeys队列的尾部)
//开始循环筛选有效的selectionKey
while ((ski = newKeys.pollFirst()) != null) {
//1.1 保证selectionKey的有效,即valid=true,在关闭selector时会将与其绑定的selectionKey都设为false
if (ski.isValid()) {
//1.2 判断channelArray是否扩容,是则扩容为原来2倍
growIfNeeded();
//1.3将selectionkey另保存到另一集合
channelArray[totalChannels] = ski;
//1.4 设置ski在pollWrapper位置,在SubSelector从pollWrapper中拉取fd进行监听事件时用到
ski.setIndex(totalChannels);
//1.5 将key对应channel的文件描述符 和感兴趣事件存入pollWrapper中,但这事件掩码为0
pollWrapper.putEntry(totalChannels, ski);
totalChannels++;
//1.6 保存文件描述符和selectionKey的对应关系
MapEntry previous = fdMap.put(ski);
assert previous == null;
}
}
// 2. 将感兴趣事件有变化的selectionKey将更新后的事件掩码重新设置到pollWrapper中
while ((ski = updateKeys.pollFirst()) != null) {
int events = ski.translateInterestOps();
int fd = ski.getFDVal();
if (ski.isValid() && fdMap.containsKey(fd)) {
int index = ski.getIndex();
assert index >= 0 && index < totalChannels;
pollWrapper.putEventOps(index, events);
}
}
}
}
//判断channelArray是否需要扩容,即存放注册到selector上的selectionKey 的集合是否需要扩容
private void growIfNeeded() {
//1.channelArray满之后,扩容为原来2被
if (channelArray.length == totalChannels) {
int newSize = totalChannels * 2; // Make a larger array
SelectionKeyImpl temp[] = new SelectionKeyImpl[newSize];
System.arraycopy(channelArray, 1, temp, 1, totalChannels - 1);
channelArray = temp;
pollWrapper.grow(newSize);
}
//2.注册到selector上的线程数每达到 每个线程处理的最大channel数时,就将用于唤醒的sourceChannel的文件描述符喝感兴趣事件放入pollWrapper
//这样每个辅助线程负责拉取的内存中的文件描述符都有wakeupSourceFd,且是第一个,这样wakeup()时主线程和所有辅助线程都会读取到发生在wakeupSourceFd上的读事件从而被唤醒
//(辅助线程poll时拉取的pollWrapper位置是从pollWrapper.pollArrayAddress + (pollArrayIndex * PollArrayWrapper.SIZE_POLLFD开始)
if (totalChannels % MAX_SELECTABLE_FDS == 0) { // more threads needed
pollWrapper.addWakeupSocket(wakeupSourceFd, totalChannels);
totalChannels++;
threadsCount++;
}
}
//用pollArray中最后一个selectionKey相关信息替换取消的selectionKey
@Override
protected void implDereg(SelectionKeyImpl ski) {
assert !ski.isValid();
assert Thread.holdsLock(this);
//1.将selectionKey从fdMap中移除
if (fdMap.remove(ski) != null) {
int i = ski.getIndex();
assert (i >= 0);
//2. 用channelArray中最有一个selectionKey覆盖取消的这个selectionKey
if (i != totalChannels - 1) {
// 2.1 覆盖channelArray元素
SelectionKeyImpl endChannel = channelArray[totalChannels-1];
channelArray[i] = endChannel;
endChannel.setIndex(i);
//2.2 覆盖pollWrapper中保存的对应的文件描述符及感兴趣事件
pollWrapper.replaceEntry(pollWrapper, totalChannels-1, pollWrapper, i);
}
ski.setIndex(-1);
//3.步骤2 已经将最后一个元素 复制了,所以最后一个元素指null,总的要select的selectionKey的数量-1
channelArray[totalChannels - 1] = null;
totalChannels--;
//4.说明最后一个元素正好是wakeup,不要被select,所以少启动一个辅助线程
if (totalChannels != 1 && totalChannels % MAX_SELECTABLE_FDS == 1) {
totalChannels--;
threadsCount--; // The last thread has become redundant.
}
}
}
package java.nio.channels.spi;
public abstract class AbstractSelector extends Selector{
private Interruptible interruptor;
//在I/O执行开始时调用,设置线程Thread中断时,selector的中断处理
protected final void begin() {
//1.初始化中断对象,设置线程中断时的回调:调用selecton具体实现的wakeup()方法
if (interruptor == null) {
interruptor = new Interruptible() {
public void interrupt(Thread ignore) {
AbstractSelector.this.wakeup();
}};
}
//2.设置中断处理对象保存到当前线程Thread中
AbstractInterruptibleChannel.blockedOn(interruptor);
//3.若当前线程已中断,调用中断处理对象的中断处理方法处理
Thread me = Thread.currentThread();
if (me.isInterrupted())
interruptor.interrupt(me);
}
//在I/O执行完成后调用,end()和begin()应一起使用
protected final void end() {
AbstractInterruptibleChannel.blockedOn(null);
}
}
// -- jdk.internal.misc.SharedSecrets --
//SharedSecrets实现了访问 Thread包私有的blockedOn方法
static void blockedOn(Interruptible intr) { // package-private
SharedSecrets.getJavaLangAccess().blockedOn(intr);
}
总结
- selector.select() 阻塞执行
- poll()拉取逻辑:调用Selector的select()方法时,会将pollWrapper的内存地址传递给内核,由内核负责轮训pollWrapper中的FD,一旦有事件就绪,将事件就绪的FD传递回用户空间,阻塞在select()的线程就会被唤醒,即每个线程轮训pollWrapper中的自己分配的FD
- selector相当于持有1个主线程和多个辅助线程,这些线程才是真正阻塞拉取就绪事件的
- wakeup()原理: https://www.cnblogs.com/yungyu16/p/13065194.html(很好的一篇文章,所以我这没有对有关pipe和wakeup()如何唤醒做过多解释),其实wakeup()之所以能唤醒阻塞等待就绪事件的线程之一:每个线程无论主线程或辅助线程都会持有wakeupSourceFd,这样wakeupSink发送数据后wakeupSource发生读事件,这样线程就被唤醒了
- wakeup()间接保证了只要有任意一个线程上分配的任意一个fd有就绪事件发生时,所有线程都会被唤醒,本轮select结束
- 可能看到这可能有点蒙,可以结合下面这张图来看selector时如何进行poll拉取事件的:
四. selector.selectedKeys()
package sun.nio.ch;
abstract class SelectorImpl extends AbstractSelector{
@Override
public final Set<SelectionKey> selectedKeys() {
ensureOpen();
return publicSelectedKeys;
}
publicSelectedKeys即为selectedKeys封装的set(构造函数中实现),调用selector.selectedKeys().iterator()
即调用selectedKeys.iterator()
static <E> Set<E> ungrowableSet(final Set<E> s) {
//间接调用s即selectedKeys
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();
}
};
}
五.iterator.remove()
到此看以看到:
- doSelect()时, 所有线程都poll完成后,会调用updateSelectedKeys(action)->SubSelector.processSelectedKeys-> SubSelector.processFDSet->SelectorImpl.processReadyEvents将就绪的selectionKey放入selectedKeys集合
- selector.selectedKeys() 从selectedKeys中获取就绪key
但是没有看到处理完就绪的selectionKeys从selectedKeys中移除啊,这样下轮进行selector.select()后selector.selectedKeys()取就绪的selectionKey时又会把上轮发生的selectionKey取到又进行一次处理,随意再在每轮处理就绪selectionKeys时就其移除iterator.remove()
总结
本文主要结合源码讲解了selector是如何拉取注册的channel上发生的就绪事件的,及 主线程和辅助线程是如何协同进行拉取的就绪事件的