NIO解读之多路复用器Selector


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的信号。可由killpthread_kill发出。

1)第二种方法可以排除,因为select一旦阻塞,应无法修改其time out时间。

2)而第三种看来只能在Linux上实现,Windows上没有这种信号通知的机制。

 

所以,看来只有第一种方法了。再回想到为什么每个Selector.open(),在Windows会建立一对自己和自己的loopbackTCP连接;在Linux上会开一对pipepipeLinux下一般都是成对打开),估计我们能够猜得出来——那就是如果想要唤醒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()是通过pipewrite 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属性中

你可能感兴趣的:(NIO)