文档章节

Java NIO Selector实现原理

AbeJeffrey
 AbeJeffrey
发布于 2017/10/25 16:02
字数 3192
阅读 56
收藏 1
点赞 0
评论 2

我们先看看传统I/O模型工作模式:

每条socket链路都由一个单独的线程负责处理,这对于大容量、高并发的应用程序来说,使用上千万个线程来处理请求几乎是不可能实现的。

多路复用IO模型的工作模式:

在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,并负责处理对应I/O事件。这对于构建高并发、大容量的服务端应用程序来说是非常有意义。

从图中我们可以看出,多路复用IO模型中,使用了一个Selector对象来管理多个通道,这是实现单个线程可以高效地处理多个socket上I/O事件的关键所在。下面开始介绍Java NIO中Selector所实现的功能和原理。

Selector简介

简单来说,Selector就是SelectableChannel对象的多路复用器。通常调用Selector类的静态方法open来创建一个选择器对象,该方法使用系统默认SelectorProvider对象的openSelector方法来创建新的选择器。当然,还可以自定义实现SelectorProvider并重写openSelector方法来创建自定义选择器。

一个Channel对象注册到选择器之后,会返回一个SelectionKey对象,这个SelectionKey对象代表这个Channel和它注册的Selector间的关系。SelectionKey中维护着两个很重要的属性:interestOps、readyOps,并通过这两个属性管理通道上注册的事件。interestOps中保存了我们希望Selector监听Channel的哪些事件,在Selector每次做select操作时,若发现该Channel有我们所监听的事件发生时,就会将感兴趣的监听事件设置到readyOps中,这样我们可以根据事件的发生执行相应的I/O操作。

Selector的重要属性

每个选择器中管理着三个SelectionKey集合:

  • 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的静态工程方法open()来创建Selector对象:

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

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

当获取到SelectorProvider实例后,调用它的openSelector()即可创建一个特定的Selector对象。

Selection操作

Selector中提供了3种类型的selection操作:

select():该方法会一直阻塞直到至少一个channel中有感兴趣的事件发生,除非当前线程发生中断或selector的wakeup方法被调用;

select(long timeout):该方法与select()类似,会一直阻塞直到至少一个channel中有感兴趣的事件发生,除非下面3种情况任意一种发生:1 设置的超时时间到达;2 当前线程发生中断;3 selector的wakeup方法被调用;

selectNow():该方法不会发生阻塞,无论是否有channel发生就绪事件,都会立即返回。

选择器中最重要的就是selection操作,当我们调用Selector的select方法时,selectedKeys集合会被更新,通过遍历selectedKeys,可以找到已经就绪的通道,从而处理各种I/O事件。select操作的大概过程如下:

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

select操作返回的值是ready集合在步骤2中被修改的键的数量,而不是selectedKeys集合中的通道总数。返回值不是已准备好的通道的总数,而是从上一个select( )调用之后进入就绪状态的通道的数量。之前的调用中就绪的,并且在本次调用中仍然就绪的通道不会被计入,而那些在前一次调用中已经就绪但已经不再处于就绪状态的通道也不会被计入。这些通道可能仍然在已选择的键的集合中,但不会被计入返回值中。返回值可能是0。

Selector唤醒

Selector中提供了使线程从被阻塞的select( )方法中优雅地退出的能力:

public abstract Selector wakeup();

如果一个线程在调用select()或select(long)方法时被阻塞,调用wakeup()会使线程立即从阻塞中唤醒;如果调用wakeup()期间没有select操作,下次调用select相关操作会立即返回。在Select期间,多次调用wakeup()与调用一次效果是一样的。关于wakeup()的实现原理可参见文章Java NIO Selector的wakeup实现原理

Selector关闭

当我们调用Selector的close()方法时,会首先执行wakeup操作,任何一个在选择操作中阻塞的线程都将被唤醒。同时会注销绑定在选择器上的所有通道,释放与此选择器相关联的任何其他资源。

如果选择已经处于关闭状态,再次调用close()方法不会由任何作用。若调用该选择器除close()和wakeup()之外的操作都会导致ClosedSelectorException异常。

SelectionKey

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集合返回的就绪状态只是一个提示,不是保证。底层的通道在任何时候都会不断改变。其他线程可能在通道上执行操作并影响它的就绪状态。

SelectionKey中使用了四个常量来代表事件类型:

SelectionKey.OP_READ:通道已经准备好进行读取;

SelectionKey.OP_WRITE:通道已经准备好写入;

SelectionKey.OP_CONNECT:通道对应的socket已经准备好连接;

SelectionKey.OP_ACCEPT:通道对应的server socket已经准备好接受一个新连接。

注册通道时,如果我们不止对一种操作感兴趣,可以用“位或”操作符将多个常量连接起来。如下:

socketChannel.register(selector, SelectionKey.OP_CONNECT|SelectionKey.OP_READ|SelectionKey.OP_WRITE);

在一次selection之后,我们可以使用以下几个方法来检测channel中什么事件已经就绪:

selectionKey.isAcceptable():是否已准备好接受新连接;
selectionKey.isConnectable():是否已准备好连接;
selectionKey.isReadable():是否已准备好读取;
selectionKey.isWritable():是否已准备好写入。

我们也可以使用相关的比特掩码来检测就绪状态,与调用上面的方法是一致的。如:

if ((selectionkey.readyOps( ) & SelectionKey.OP_READ) != 0) {
    readBuffer.clear( ); 
    key.channel( ).read (readBuffer); 
    ... 
}

一个selectionKey被创建后将保持有效,调用selectionKey的cancel()方法或关闭其通道或关闭其选择器将导致其失效。我们可以调用isValid( )方法来检查selectionKey是否仍然有效。当我们调用selectionKey的cancel()方法后,它将被放在相关的选择器的cancelledKeys集合中。注册关系不会立即被取消,但是selectionKey会立即失效。当再次调用select( )方法时(或者一个正在进行的select()调用结束时),cancelledKeys中的被取消的键将被清理掉。

selectionKey除了维护Channel和Selector的注册关系外,还提供了保存“附件”的功能,并提供方法访问它。这是一种允许我们将任意对象与键关联的便捷方法。这个对象可以引用任何对象,例如业务对象、会话句柄、其他通道等等。当我们在遍历与选择器相关的键时,可以使用附加在selectionKey上的对象句柄来获取相关的上下文。attach( )方法将在selectionKey中保存所提供的对象的引用,attachment( )方法则用来获取与selectionKey关联的附件句柄。

关于SelectionKey还有最后一点需要注意,SelectionKey是线程安全的。修改interest集合的操作是通过Selector对象进行同步的,而选择器所使用的锁策略是依赖于具体实现的。因此如果Selector正在进行选择操作,则读取或写入interest集可能会阻塞不确定的时间。

示例代码

selector=Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
    selector.select();
    Set<SelectionKey> selectionKeys=selector.selectedKeys();
    Iterator<SelectionKey> iterator=selectionKeys.iterator();
    while(iterator.hasNext()){
        SelectionKey selectionKey=iterator.next();
        if(selectionKey.isAcceptable()){
            // a connection was accepted by a ServerSocketChannel.
        }else if(selectionKey.isReadable()){
            // a channel is ready for reading
        }else if(selectionKey.isWritable()){
           // a channel is ready for writing
        }
        iterator.remove();
    }
}

完整的示例代码可移步https://github.com/JeffreyHy/jeffery-nio-study/tree/master/nio-demo

欢迎指出本文有误的地方,转载请注明原文出处https://my.oschina.net/7001/blog/1556102

© 著作权归作者所有

共有 人打赏支持
AbeJeffrey
粉丝 29
博文 43
码字总数 116062
作品 0
杭州
高级程序员
加载中

评论(2)

AbeJeffrey
AbeJeffrey
先看这篇文章或其他Selector相关的内容,再深入源码会比较好
平时不吹牛
平时不吹牛
我觉得应该先带着你的这篇总结看原理,不然先原理后总结,我就会看完后面忘记前面。现在掌握5,6分熟,我在看看。 vx : dengqun2011
Java NIO原理 图文分析及代码实现

Java NIO原理图文分析及代码实现 前言: 最近在分析hadoop的RPC(Remote Procedure Call Protocol ,远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术...

囚兔
2015/04/29
0
0
Java NIO原理图文分析及代码实现

前言: 最近在分析hadoop的RPC(Remote Procedure Call Protocol ,远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。可以参考:http://baik...

SunnyWu
2014/11/05
0
1
java NIO原理及通信模型

Java NIO是在jdk1.4开始使用的,它既可以说成“新IO”,也可以说成非阻塞式I/O。下面是java NIO的工作原理: 由一个专门的线程来处理所有的IO事件,并负责分发。 事件驱动机制:事件到的时候...

柳哥
2015/02/15
0
4
Java NIO原理图文分析及代码实现

Java IO 在Client/Server模型中,Server往往需要同时处理大量来自Client的访问请求,因此Server端需采用支持高并发访问的架构。一种简单而又直接的解决方案是“one-thread-per-connection”。...

只想一个人静一静
2014/02/22
0
1
Java NIO原理图文分析及代码实现

前言: 最近在分析hadoop的RPC(Remote Procedure Call Protocol ,远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。可以参考:http://baik...

phacks
2015/08/19
0
0
Java NIO之Selector(选择器)

历史回顾: Java NIO 概览 Java NIO 之 Buffer(缓冲区) Java NIO 之 Channel(通道) 其他高赞文章: 面试中关于Redis的问题看这篇就够了 一文轻松搞懂redis集群原理及搭建与使用 超详细的Jav...

山川_84b6
05/16
0
0
Java NIO系列教程(一) Java NIO 概述

Java NIO 由以下几个核心部分组成: Channels Buffers Selectors 虽然Java NIO 中除此之外还有很多类和组件,但在我看来,Channel,Buffer 和 Selector 构成了核心的API。其它组件,如Pipe和...

只想一个人静一静
2014/02/22
0
0
最近仔细研究了一下Java的NIO以及线程并发,搞清了点思路,特作笔记如下(NIO篇)

[转]http://www.cnblogs.com/feidao/archive/2005/07/15/193788.html 因为前段时间的项目需要写一些高性能服务器,结果写出来的结果是五花八门,我们要求使用NIO编写异步服务器,但是竟然有人...

风林火山
2010/12/26
0
1
Apache Mina 网络通信

Apache Mina Server 是一个网络通信应用框架,也就是说,它主要是对基于TCP/IP、UDP/IP协议栈的通信框架(当然,也可以提供JAVA 对象的序列化服务、虚拟机管道通信服务等),Mina 可以帮助我...

Mr&Cheng
2013/01/20
0
0
Java NIO 系列教程 -- delete

(一) Java NIO 概述 Java NIO 由以下几个核心部分组成: Channels Buffers Selectors 虽然Java NIO 中除此之外还有很多类和组件,但在我看来,Channel,Buffer 和 Selector 构成了核心的A...

数据之美
2013/06/09
0
4

没有更多内容

加载失败,请刷新页面

加载更多

下一页

用 Python 实现打飞机,让子弹飞吧!

所用技术和软件 python 2.7 pygame 1.9.3 pyCharm 准备工作 安装好 pygame 在第一次使用 pygame 的时候,pyCharm 会自动 install pygame。 下载好使用的素材。 技术实现 初始化 pygame 首先要...

猫咪编程
7分钟前
0
0
MySQL的行锁和表锁

简单总结一下行锁和表锁。 行锁 每次操作锁住一行数据。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。 表锁 每次操作锁住整张表。开销小,加锁快;不会出...

to_ln
10分钟前
0
0
Java IO类库之字节数组输入流ByteArrayInputStream

一、ByteArrayInputStream字节数组输入流介绍 ByteArrayInputStream是字节数组输入流,继承自InputStream。它的内部包含一个缓冲区,是一个字节数组,缓冲数组用于保存从流中读取的字节数据,...

老韭菜
11分钟前
0
0
iOS安全应该做哪些事情

1. 尽量使用HTTPS协议。 2. 密码提交的时候,密码使用SHA256加密后传输,MD5等经过哈希碰撞已经可以推算出原文。 3. 密码提交的时候,可以加盐。 4. 密码保存在本地的时候,尽量使用钥匙串保...

HOrange
18分钟前
0
0
react native 注意事项

1. 环境参考官网 android studio 必装 java jdk安装 1.8版本(环境建议自己一步一步配置,切记不要 apt ) 2.有改变编译内容发现 会白屏,然后APP消失,请卸载原来的测试 appinfo (连续两次...

304158
24分钟前
0
0
FOMO游戏代码解析

源代码在此处

怎当她临去时秋波那一转
29分钟前
1
0
EOS智能合约与DApp开发入门

EOS的是Block.One主导研发的一个区块链底层公链系统,它专门为支撑商业去中心化 应用(Decentralized Application)而设计,其代码开源。 比特币被称为区块链1.0,因为它开辟了数字加密货币的...

笔阁
41分钟前
1
0
编译cjson到dll

https://blog.csdn.net/mengzhisuoliu/article/details/52203724 编译完成后 是纯lua实现的json decode 的10倍以上...

梦想游戏人
51分钟前
0
0
JS基础- Date 对象

Date 对象 Date 对象用于处理日期和时间。 创建 Date 对象的语法: var myDate=new Date() 注释:Date 对象会自动把当前日期和时间保存为其初始值。 Date 对象属性 属性 描述 constructor 返...

ZHAO_JH
53分钟前
0
0
Python数据分析numpy(1)

Python开源的科学计算基础库 1.表示N维数组对象ndarray 2.线性代数、傅里叶变换、随机数生成 3.广播函数,整合c++、c 一.数据的维度 1.数据 2.数据维度 3.一维数据 (1)特点 (2)Python中的...

十年磨一剑3344
56分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部