Java基础---nio学习

本文详细解读Selector在Java NIO中的关键操作,包括创建、注册、select方法的使用以及wakeup功能。探讨了Selector的重要属性如keys、selectedKeys和cancelledKeys,以及如何通过publicKeys和publicSelectedKeys视图进行操作。
摘要由CSDN通过智能技术生成

目录

Selector的重要属性

Selector的创建

Selector的注册

Selector的select的操作

Selector的唤醒wakeup()

Selector的重要属性

  • keys:该集合中保存了所有注册到当前选择器上的通道的SelectionKey对象;
  • selectedKeys:该集合中保存了上一次Selector选择期间,发生了就绪事件的通道的SelectionKey对象集合,它始终是keys的子集。
  • cancelledKeys:该集合保存了已经被取消但其关联的通道还未被注销的SelectionKey对象集合,它始终是keys的子集。

初始化Selector对象时,这三个集合都为空。当我们调用Channel的register方法将通道注册到选择器时,一个SelectionKey对象会被加入到keys集合;

调用通道的Close方法或直接调用选择器的cancel方法则会将一个SelectionKey对象添加到cancelledKeys集合,选择器下一次做选择操作时,将会清空cancelledKeys中保存的选择键,并从keys集合中删除;

选择器做选择操作时,具有就绪事件的SelectionKey对象会被加入到selectedKeys集合中。

每个Selector中还维护了publicKeys和publicSelectedKeys两个视图,供客户端使用。publicKeys是keys的视图,调用Selector的keys()方法返回的就是publicKeys,publicKeys不支持添加和删除操作;publicSelectedKeys是selectedKeys的视图,它是是个不可增长的集合,即不支持add操作,但支持remove操作,调用publicSelectedKeys集合的remove操作实际是从selectedKeys中删除一个SelectionKey对象。我们可以调用Selector的selectedKeys()方法访问publicSelectedKeys。

Selector的创建

Selector selector = Selector.open();

open 方法:open方法负责向SPI发出请求,获取一个SelectorProvider实例。然后对应的provider调用openSelector,获取具体实现的selector

public static Selector open() throws IOException { return SelectorProvider.provider().openSelector(); } 

provider()方法:SelectorProvider的静态工厂方法 provider()决定由哪个SelectorProvider对象来创建给定的Selector实例,通常是一个DefaultSelectorProvider实例。不同操作系统对应着不同的sun.nio.ch.DefaultSelectorProvider,Linux下DefaultSelectorProvider.create()会生成一个sun.nio.ch.EPollSelectorProvider类型的SelectorProvider,Windows环境下则生成sun.nio.ch.WindowsSelectorProvider类型的SelectorProvider。

    public static SelectorProvider provider() {
        synchronized (lock) {
            if (provider != null)
                return provider;
            return AccessController.doPrivileged(
                new PrivilegedAction<SelectorProvider>() {
                    public SelectorProvider run() {
                            if (loadProviderFromProperty())
                                return provider;
                            if (loadProviderAsService())
                                return provider;
                            provider = sun.nio.ch.DefaultSelectorProvider.create();
                            return provider;
                        }
                    });
        }
    }

Selector的注册

根据channel与selector生成相应的SelectionKeyImpl,其构造方法就是简单的保存了传递进来的参数,并且将所需要attach的object进行保存。之后,调用子类的implRegister()方法。然后修改它的的interestOpts

    protected final SelectionKey register(AbstractSelectableChannel var1, int var2, Object var3) {
        if (!(var1 instanceof SelChImpl)) {
            throw new IllegalSelectorException();
        } else {
            SelectionKeyImpl var4 = new SelectionKeyImpl((SelChImpl)var1, this);
            var4.attach(var3);
            Set var5 = this.publicKeys;
            synchronized(this.publicKeys) {
                this.implRegister(var4);
            }

            var4.interestOps(var2);
            return var4;
        }
    }

SelectionKey对象代表着一个Channel和它注册的Selector间的关系。其channel( )方法可返回与该键相关的SelectableChannel对象,而selector( )则返回相关的Selector对象。SelectionKey中包含两个重要属性,两个以整数形式进行编码的比特掩码:

  • interestOps:代表对注册Channel所感兴趣的事件集合。interest集合是使用注册通道时给定的值初始化的,可以通过调用键对象的interestOps( int ops)方法修改。同时,可以调用键对象的interestOps()方法获取当前interest集合。当相关的Selector上的select( )操作正在进行时改变键的interest集合,不会影响那个正在进行的选择操作。所有更改将会在select( )的下一个调用中体现出来;
  • readyOps:代表interest集合中从上次调用select( )以来已经就绪的事件集合,它是interestOps的子集。注册通道时,初始化为0,只有在选择器选择操作期间可能被更新。可以调用键对象的readyOps()方法获取当前ready集合。需注意的是ready集合返回的就绪状态只是一个提示

Selector的select的操作

  • select():该方法会一直阻塞直到至少一个channel中有感兴趣的事件发生,除非当前线程发生中断或selector的wakeup方法被调用;
  • select(long timeout):该方法与select()类似,会一直阻塞直到至少一个channel中有感兴趣的事件发生,除非下面3种情况任意一种发生:1 设置的超时时间到达;2 当前线程发生中断;3 selector的wakeup方法被调用;
  • selectNow():该方法不会发生阻塞,无论是否有channel发生就绪事件,都会立即返回。

select()大致的过程

  1. 检查cancelledKeys集合,如果它非空,从keys集合中移除所有存在于cancelledKeys集合中的SelectionKey对象,并将注销其通道,同时清空cancelledKeys;
  2. 向内核发起一个系统调用进行查询,以确定选择器上注册的每个通道所关心的事件是否就绪。如果没有通道已经准备好,线程可能会一直阻塞、阻塞指定时间,或立即返回,这主要依赖于特定select方法的调用;
  3. 系统调用返回,再次检查cancelledKeys集合;
  4. 系统调用返回后,对于那些没有就绪事件的通道将不会有任何的操作,对于那些已经有就绪事件的通道:1  如果通道的SelectionKey还未加入selectedKeys集合,将其添加到selectedKeys集合中,并修改ready集合,以便准确地标识该通道当前有哪些准备好的操作。先前记录在ready集合中的任何就绪信息都会被抛弃;2 通道的SelectionKey已经存在于selectedKeys集合,修改ready集合,以便准确地标识该通道当前有哪些准备好的操作。所有之前记录在ready集合中已经不再是就绪状态的操作不会被清除。事实上,所有的比特位都不会被清理。由操作系统决定的ready集合是与之前的ready集合按位分离的,一旦键被放置于选择器的已选择的键的集合中,它的ready集合将是累积的。比特位只会被设置,不会被清理。

select操作返回的值是ready集合在步骤2中被修改的键的数量,而不是selectedKeys集合中的通道总数。是这一次就绪的数量

Selector的唤醒wakeup()

  • selector.select()是阻塞的,通常情况下,只有注册在selector上的channel有事件就绪时,select()才会从阻塞中被唤醒,处理就绪事件。如果想在没有事件就绪时唤醒select线程,需要调用wakeup()方法。

wakeup()实现的功能:

  • 如果一个线程在调用select()或select(long)方法时被阻塞,调用wakeup()会使线程立即从阻塞中唤醒;
  • 如果调用wakeup()期间没有select操作,下次调用select相关操作会立即返回,不会执行poll(),包括调用selectNow()。
  • 在select期间,多次调用wakeup()与调用一次效果是一样的。
  • 如果调用wakeup()期间没有select操作,后续若先调用一次selectNow(),再次调用select()则会导致阻塞。

wakeup()实现:

  • 实例化SelectorImpl时,会调用Pipe.open()创建一个管道实例wakeupPipe,并从wakeupPipe中获取wakeupSourceFd和wakeupSinkFd两个文件描述符,wakeupSourceFd为read端FD,wakeupSinkFd为write端FD,然后将wakeupSourceFd加入pollWrapper中。
  • pollWrapper的作用是保存当前selector对象上注册的FD,当调用Selector的select()方法时,会将pollWrapper的内存地址传递给内核,由内核负责轮训pollWrapper中的FD,一旦有事件就绪,将事件就绪的FD传递回用户空间,阻塞在select()的线程就会被唤醒。
  • wakeup通过wakeupSinkFd发送信号到wakeupSourceFd,让Selector的select()方法检测到wakeupSourceFd已经就绪,从而打破了阻塞,做到唤醒。

 

windows环境下wakeup()的实现原理,它通过一个可写的SinkChannel和一个可读的SourceChannel组成的pipe来实现唤醒的功能

Linux环境则使用其本身的Pipe来实现唤醒功能。

无论windows还是linux,wakeup的思想是完全一致的,只不过windows没有Pipe这种信号通知的机制,所以通过TCP来实现了Pipe。

每创建一个Selector对象,都会创建一个Pipe实例,这会导致消耗两个文件描述符FD和两个端口号

WindowsSelectorImpl 源码解析

初始化的时候创建了pipe

对象成员,一个通道,用作唤醒    
private final Pipe wakeupPipe = Pipe.open();

初始化代码 将通道的读端加入了pollWrapper

    WindowsSelectorImpl(SelectorProvider var1) throws IOException {
        // 初始化Selector
        super(var1);
        this.wakeupSourceFd = ((SelChImpl)this.wakeupPipe.source()).getFDVal();
        SinkChannelImpl var2 = (SinkChannelImpl)this.wakeupPipe.sink();
        // 不适用延迟,有任何字节立马发送
        var2.sc.socket().setTcpNoDelay(true);
        this.wakeupSinkFd = var2.getFDVal();
        // 将wakeupSourceFd加入pollWrapper,关注通道是否有数据
        this.pollWrapper.addWakeupSocket(this.wakeupSourceFd, 0);
    }

wakeup代码

    public Selector wakeup() {
        Object var1 = this.interruptLock;
        synchronized(this.interruptLock) {
            // 收到中断。多次收到中断并不会多次发送
            if (!this.interruptTriggered) {
                // 向wakeupSinkFd 发送一个字节,让wakeupSourceFd就绪
                this.setWakeupSocket();
                this.interruptTriggered = true;
            }
            return this;
        }
    }

 在select过程中 会首先判断是否收到了中断,如果收到中断就不会poll了,会直接重置中断,然后返回0。

参考链接

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值