Selector类的结构图如下所示:
Selector是JDK的NIO中最重要的类之一,当我们通过Selector.open()方法打开一个多路复用器的时候实际上执行的open方法为
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
调用了SelectorProvider.provider()方法,方法代码如下:
public static SelectorProvider provider() {
synchronized (lock) {
if (provider != null)
return provider;
return AccessController.doPrivileged(
new PrivilegedAction
public SelectorProvider run() {
if (loadProviderFromProperty())
return provider;
if (loadProviderAsService())
return provider;
provider = sun.nio.ch.DefaultSelectorProvider.create();
return provider;
}
});
}
}
可以看出这个方法首先通过加锁保证了静态的属性provider 如果不为空,才去创建一个,这个方法对于不同的操作系统平台会返回不同的实例,对于Windows返回的就是WindowsSelectorProvider这个Provider
然后调用了WindowsSelectorProvider的openSelector方法创建了WindowsSelectorImpl也就是我们真正的多路复用器实现类,接着WindowsSelectorImpl的构造方法:
WindowsSelectorImpl(SelectorProvider sp) throws IOException {
super(sp);
pollWrapper = new PollArrayWrapper(INIT_CAP);
wakeupPipe = Pipe.open();
wakeupSourceFd = ((SelChImpl)wakeupPipe.source()).getFDVal();
// Disable the Nagle algorithm so that the wakeup is more immediate
SinkChannelImpl sink = (SinkChannelImpl)wakeupPipe.sink();
(sink.sc).socket().setTcpNoDelay(true);
wakeupSinkFd = ((SelChImpl)sink).getFDVal();
pollWrapper.addWakeupSocket(wakeupSourceFd, 0);
}
首先通过 super(sp); 调用了父类的SelectorImpl的构造方法将SelectorProvider的实现类WindowsSelectorProvider传入,SelectorImpl的构造方法如下:
protected SelectorImpl(SelectorProvider sp) {
super(sp);
keys = new HashSet
selectedKeys = new HashSet
if (Util.atBugLevel("1.4")) {
publicKeys = keys;
publicSelectedKeys = selectedKeys;
} else {
publicKeys = Collections.unmodifiableSet(keys);
publicSelectedKeys = Util.ungrowableSet(selectedKeys);
}
}
他的第一行代码也调用了父类AbstractSelector的构造方法:代码如下:
protected AbstractSelector(SelectorProvider provider) {
this.provider = provider;
}
他只是将SelectorProvider 的实现类保存到了自己的实例变量中,可见AbstractSelector类中保存了SelectorProvider 的实现,接着看SelectorImpl构造方法的其他代码:
他构造了keys和selectedKeys变量。接着看WindowsSelectorImpl的构造方法:
接着构造了pollWrapper变量,接着看wakeupPipe = Pipe.open();这是一行关键的代码,看看Pipe.open()方法,代码如下:
public static Pipe open() throws IOException {
return SelectorProvider.provider().openPipe();
}
他同样也是通过SelectorProvider.provider()创建的上文说过这个对象的创建,这个方法定义在SelectorProviderImpl这个abstract类中,同样几个其他的DatagramChannel,ServerSocketChannel,SocketChannel都是定义在SelectorProviderImpl这个的类中,看上文的继承结构图可以看出这个类是WindowsSelectorProvider的父类,实际上WindowsSelectorProvider只是实现了抽象类SelectorProviderImpl中的一个WindowsSelectorProvider方法而已,SelectorProviderImpl的openPipe方法如下:
public Pipe openPipe() throws IOException {
return new PipeImpl(this);
}
接着看PipeImpl的构造方法,Pipe类的继承结构如下所示:
PipeImpl类的构造方法如下:
PipeImpl(SelectorProvider sp) {
long pipeFds = IOUtil.makePipe(true);
int readFd = (int) (pipeFds >>> 32);
int writeFd = (int) pipeFds;
FileDescriptor sourcefd = new FileDescriptor();
IOUtil.setfdVal(sourcefd, readFd);
source = new SourceChannelImpl(sp, sourcefd);
FileDescriptor sinkfd = new FileDescriptor();
IOUtil.setfdVal(sinkfd, writeFd);
sink = new SinkChannelImpl(sp, sinkfd);
}
首先调用IOUtil.makePipe这个native方法,通过他的注释我们可以看出他创建了一个pipe管道的两个文件描述符对象,read端是返回值的高32位,write端是返回值的低32位,
/**
* Returns two file descriptors for a pipe encoded in a long.
* The read end of the pipe is returned in the high 32 bits,
* while the write end is returned in the low 32 bits.
*/
static native long makePipe(boolean blocking);
接下来看PipeImpl的构造方法中将返回的文件描述符的地址进行了相应的赋值,接着创建了一个sourcefd 的文件描述符对象,将他与readFd关联上,将sinkfd文件描述符对象和writeFd关联
接着创建了SourceChannelImpl和SinkChannelImpl对象,SourceChannelImpl类就对应了pipe读一端的channel,SinkChannelImpl类就对应了pipe写一端的channel.
这样PipeImpl对象创建完毕返回赋值带WindowsSelectorImpl类的wakeupPipe属性,WindowsSelectorImpl的wakeupSourceFd属性就对应了刚才创建的Pipe对象的source,
wakeupSinkFd属性就对应了刚才创建的Pipe对象的sink
接着将wakeupSourceFd这个文件描述符加入到pollWrapper对象中,构造方法就结束了。
pollWrapper对象中保存的文件描述符对象就是调用多路复用器select方法时操作系统要扫描的文件描述符列表。
其实WindowsSelectorImpl创建的Pipe对象的就是为了自己唤醒自己而已,对于调用了多路复用器对象的select方法时,是一直阻塞的,实际上操作系统就是在轮训pollWrapper对象中注册的文件描述符对象。试想一下如果这个时候想加入一个新的Channel,那么势必得让select方法返回,一个阻塞在select上的线程有以下三种方式可以被唤醒:
1) 有数据可读/写,或出现异常。
2) 阻塞时间到,即time out。
3) 收到一个non-block的信号。可由kill或pthread_kill发出。
1)第二种方法可以排除,因为select一旦阻塞,应无法修改其time out时间。
2)而第三种看来只能在Linux上实现,Windows上没有这种信号通知的机制。
所以,看来只有第一种方法了。再回想到为什么每个Selector.open(),在Windows会建立一对自己和自己的loopback的TCP连接;在Linux上会开一对pipe(pipe在Linux下一般都是成对打开),估计我们能够猜得出来——那就是如果想要唤醒select,只需要朝着自己的这个loopback连接发点数据过去,于是,就可以唤醒阻塞在select上的线程了。
这时再来看看WindowsSelectorImpl. Wakeup():
public Selector wakeup() {
synchronized (interruptLock) {
if (!interruptTriggered) {
setWakeupSocket();
interruptTriggered = true;
}
}
return this;
}
private void setWakeupSocket() {
setWakeupSocket0(wakeupSinkFd);
}
private native void setWakeupSocket0(int wakeupSinkFd);
可见wakeup()是通过pipe的write 端send(scoutFd, &byte, 1, 0),发生一个字节1,来唤醒poll()。所以在需要的时候就可以调用selector.wakeup()来唤醒selector。
对于windows,每当调用一次Selector的open方法就建立了两个TCP的链接,一个Server绑定了一个随机的端口号,一个client连接,server和client相连,如果要是实现wakeup,client就给这个server发送一点儿数据就OK 了。
对于linux使用的是pipe管道来实现的。
下面来说说Selector.select方法:
public int select() throws IOException {
return select(0);
}
public int select(long timeout)
throws IOException
{
if (timeout < 0)
throw new IllegalArgumentException("Negative timeout");
return lockAndDoSelect((timeout == 0) ? -1 : timeout);
}
private int lockAndDoSelect(long timeout) throws IOException {
synchronized (this) {
if (!isOpen())
throw new ClosedSelectorException();
synchronized (publicKeys) {
synchronized (publicSelectedKeys) {
return doSelect(timeout);
}
}
}
}
他最后调用到了WindowsSelectorImpl的doSelect方法:
protected int doSelect(long timeout) throws IOException {
if (channelArray == null)
throw new ClosedSelectorException();
this.timeout = timeout; // set selector timeout
processDeregisterQueue();
if (interruptTriggered) {
resetWakeupSocket();
return 0;
}
// Calculate number of helper threads needed for poll. If necessary
// threads are created here and start waiting on startLock
adjustThreadsCount();
finishLock.reset(); // reset finishLock
// Wakeup helper threads, waiting on startLock, so they start polling.
// Redundant threads will exit here after wakeup.
startLock.startThreads();
// do polling in the main thread. Main thread is responsible for
// first MAX_SELECTABLE_FDS entries in pollArray.
try {
begin();
try {
subSelector.poll();
} catch (IOException e) {
finishLock.setException(e); // Save this exception
}
// Main thread is out of poll(). Wakeup others and wait for them
if (threads.size() > 0)
finishLock.waitForHelperThreads();
} finally {
end();
}
// Done with poll(). Set wakeupSocket to nonsignaled for the next run.
finishLock.checkForException();
processDeregisterQueue();
int updated = updateSelectedKeys();
// Done with poll(). Set wakeupSocket to nonsignaled for the next run.
resetWakeupSocket();
return updated;
}
来看看几个关键的方法:
private void adjustThreadsCount() {
if (threadsCount > threads.size()) {
// More threads needed. Start more threads.
for (int i = threads.size(); i < threadsCount; i++) {
SelectThread newThread = new SelectThread(i);
threads.add(newThread);
newThread.setDaemon(true);
newThread.start();
}
} else if (threadsCount < threads.size()) {
// Some threads become redundant. Remove them from the threads List.
for (int i = threads.size() - 1 ; i >= threadsCount; i--)
threads.remove(i).makeZombie();
}
}
在分析ServerSocketChannel的regist方法时分析过如果注册的channel数量超过了1024那么就要启动一个新的帮助线程来出来,这个方法就是根据threadsCount属性的值来启动相应的线程,那么创建的线程就是从一个索引的位置(1024,2048顺序递增)起轮训pollWrapper对应的索引中的文件描述符的,也就是调用doselct方法的主线程轮训的是pollWrapper从0到1023索引中的fd,剩下的有子线程相应的处理,他们都是阻塞在
subSelector.poll();方法上
private int poll() throws IOException{ // poll for the main thread
return poll0(pollWrapper.pollArrayAddress,
Math.min(totalChannels, MAX_SELECTABLE_FDS),
readFds, writeFds, exceptFds, timeout);
}
private native int poll0(long pollAddress, int numfds,
int[] readFds, int[] writeFds, int[] exceptFds, long timeout);
当线程被唤醒时readFds,writeFds,exceptFds就被相应的赋值
主线程和子线程是通过startLock和finishLock来交互的,具体的就是如果主线程被唤醒了而没有一个子线程被唤醒,那么主线程就要等待至少一个子线程被唤醒,当有一个子线程被唤醒时他就唤醒其他的子线程和主线程一起返回。
接下来看看updateSelectedKeys方法:
private int updateSelectedKeys() {
updateCount++;
int numKeysUpdated = 0;
numKeysUpdated += subSelector.processSelectedKeys(updateCount);
for (SelectThread t: threads) {
numKeysUpdated += t.subSelector.processSelectedKeys(updateCount);
}
return numKeysUpdated;
}
主线程和子线程都要调用到 subSelector.processSelectedKeys方法上,代码如下:
private int processSelectedKeys(long updateCount) {
int numKeysUpdated = 0;
numKeysUpdated += processFDSet(updateCount, readFds,
PollArrayWrapper.POLLIN,
false);
numKeysUpdated += processFDSet(updateCount, writeFds,
PollArrayWrapper.POLLCONN |
PollArrayWrapper.POLLOUT,
false);
numKeysUpdated += processFDSet(updateCount, exceptFds,
PollArrayWrapper.POLLIN |
PollArrayWrapper.POLLCONN |
PollArrayWrapper.POLLOUT,
true);
return numKeysUpdated;
}
他们就是将readFds,writeFds与注册的SelectionKeyImpl对象关联上设置到相应的事件保存到SelectorImpl对象的selectedKeys属性中