7. Java NIO Selector

Selector是Java NIO的一个组件,可以检查一个或者多个NIO channel,并且决定哪个channel已经准备好去读写了。通过这种方式,单个线程可以管理多个channel,当然也可以管理多个网络连接。

为什么使用selector ?

使用单个线程处理多个channel的优势是,你只需要很多的线程去处理大量channel。实际上,你可以只使用一个线程来处理所有的channel。对操作系统来说切换线程是需要大量开销的,而且每个线程都会占用一些资源(内存)。所以你使用的线程越少越好(译者注:达到目的的情况下,尽可能使用少的线程)。

不过要记住,当代操作系统和CPU的多任务处理性能已经越来越高了,所以多线程的开销已经变得越来越小了。实际上,如果CPU有多核,你不进行多任务处理则是对CPU性能的浪费。不管怎么样,关于这部分内容的讨论会在其他教程中给出。但是足够的是,你可以使用selector来达到使用单线程处理多个channel的目的。

下面的示意图是表示单个线程来处理三个channel:

1个线程处理3个channel

创建Selector

通过调用Selector.open()来创建Selector:


Selector selector = Selector.open();

使用Selector注册Channel

为了通过Selector来使用Channel,你需要使用Selector来注册Channel。这可以通过调用SelectableChannel.register()方法来实现,如下所示:


channel.configureBlocking(false);

 SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

Channel要被Selector使用,必须要是非阻塞(non-blocking)模式。这意味着你不能使用Selector来管理FileChannel,因为它不能选择非阻塞模式。但是socket channel是没有问题的。

注意register()的第二个参数。这是一个“偏好设置”,意思是你向监听channel的什么事件,通过Selector.你可以选择不同的监听事件,共有四种不同的监听事件:

  • Connect
  • Accept
  • Read
  • Write

所以,一个channel已经成功连接到另一个server就是上面的connect事件。一个server socket正在等待一个连接接入的过程就是上面的accept事件。一个channel有数据准备好去被读取就是read事件。一个channel已经准备好让你往里面写入数据,这就是write事件。

这四个事件用下面四个常量来表示:


SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE

如果你想注册多个事件,可以像下面这样:


int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;   

SelectionKey's

如上面所述,当你用selector注册一个channel,register()方法会返回一个SelectionKey对象。这个对象包含一些有意思的属性:

  • interest set
  • ready set
  • Channel
  • Selector
  • 附加对象 (optional)

下面来具体介绍这些属性。

Interest Set

Interest Set表示的是你扫描的事件的集合。你可以通过SelectionKey来读写这个interest set像下面这样:


int interestSet = selectionKey.interestOps();

 boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;

如你所见,你可以&操作给出的SelectionKey常量,去找出某一事件是否在interest set中。

Ready Set

ready set是channel准备好的操作的集合。主要是在selection后访问ready set。至于selection,会在后面给出解释。你可以像下面这样访问ready set:


int readySet = selectionKey.readyOps();

你可以用interest set以同样的方式来测试channe已经做好什么事件的准备了。但是,你可以利用下面的四个方法来代替,它们都会返回一个布尔值:


selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

Channel 和 Selector

从SelectionKey来访问channel和seector是无关紧要的,下面展示了如何进行此操作:


Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();   

附加对象

你可以将一个对象附加到SelectionKey中,或者给出更多的信息添加至channel,这可以很方便的途径去识别一个给定的channel。例如,你可以添加使用的buffer,或者一个包含更多数据的对象。下面是相关例子:


selectionKey.attach(theObject);

 Object attachedObj = selectionKey.attachment();

你也可以使用register()方法,在使用Selector注册channel时添加这个对象。如下所示:


SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

通过Selector选择Channel

当你用Selector注册了一个或多个channel,你可以调用select()方法以及其重载的方法。这些方法会返回你监听的事件(connect, accept, read 和 write)。换句话说,你可以从这个方法接受到已经准备好的事件。下面是select()方法以及其相关重载:


int select()
int select(long timeout)
int selectNow()

select()方法会一直阻塞,直到至少一个你注册的channel已经准备好相应事件。

select(long timeout)和select()方法做了同样的事儿,不过它是阻塞一段时间(多长时间取决于你传入的事件参数)。

selectNow() 一点也不会阻塞。不管channel准备好没有,它立即回返回。

select() 方法返回的int值表示有多少channel已经准备好。即:在上一次调用select()方法之后一共有多少channel已经准备好。如果你调用select()方法,它返回 1,因为此时只有一个channel准备好,那么你再一次调用select(),然后期间又有一个channel准备好,它会再返回1,而不是2。如果你没有对第一个准备好的channel有任何操作,你现在其实已经有两个准备好的channel了,但是只有一个channel是你两次调用之间准备好的channel。

selectedKeys()

调用select()方法之后,它会返回一个int值,这表示一个或多个channel已经准备好了,你可以通过“selected key set”访问准备好的channel,即调用selectedKeys()方法:下面是如何操作的:


Set<SelectionKey> selectedKeys = selector.selectedKeys();   

当你注册一个channel时候,Channel.register()返回一个SelectionKey对象。这个key表示表示的是这个channel使用了哪个selector来注册的。可以通过selectedKeySet()方法来访问key。

你可以迭代它来访问准备好的channel:


Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {

    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
    } else if (key.isConnectable()) {
        // a connection was established with a remote server.
    } else if (key.isReadable()) {
        // a channel is ready for reading
    } else if (key.isWritable()) {
        // a channel is ready for writing
    }
    keyIterator.remove();
}

这个循环迭代selected key集合中的key。她再测试每个key以便去确定通过key引用的channel是否已经准备好。

注意,在循环的最后调用了keyIterator.remove()方法。Selector本身并没有从selected key集合中删除SelectionKey实例。当你操作完channel后你需要这么做。下次channel准备好后,selector会再次将其添加至select key集合中。

方法SelectionKey.channel()返回的channel可以被强转成你需要使用的channel,例如ServerSocketChannel o或 SocketChannel

wakeUp()

调用select()方法的线程被阻塞后,你可以让这个线程离开select()方法,即使channel没有准备好呢。这需要另一个线程在selector上调用Selector.wakeup()方法。

如果一个不同的线程调用了wakeup()方法,并且当前已经没有线程在select()阻塞了,那么下一个调用select()方法的线程会立即唤醒(也就是不阻塞了)。

close()

当使用完Selector你可以调用她的close()方法。这个操作会关闭selector,并且使所有在此selector上注册的SelectKey实例失效。channel本身并没有关闭。

完整的Selector例子

下面是一个完成的例子,包括打开Selector,并使用其注册channel(channel实例化不考虑),然后保持selector监控四个事件(accept, connect, read, write)。


Selector selector = Selector.open();

channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

while(true) {
  int readyChannels = selector.select();

  if(readyChannels == 0) continue;

  Set<SelectionKey> selectedKeys = selector.selectedKeys();

  Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

  while(keyIterator.hasNext()) {

    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
    } else if (key.isConnectable()) {
        // a connection was established with a remote server.
    } else if (key.isReadable()) {
        // a channel is ready for reading
    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
  }
}


想要查看此教程的目录请点击:Java NIO教程目录贴地址

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,603评论 5 465
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,422评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,478评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,116评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,037评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,832评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,075评论 3 389
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,685评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,976评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,028评论 2 315
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,830评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,584评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,101评论 3 301
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,229评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,521评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,121评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,314评论 2 339

推荐阅读更多精彩内容

  • Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java I...
    JackChen1024阅读 7,520评论 1 143
  • Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java I...
    zhisheng_blog阅读 1,109评论 0 7
  • 选择器是Java NIO组件,它可以检查一个或多个NIO通道,并确定哪些通道准备好 阅读或写作。 这样一个单一的线...
    FantJ阅读 734评论 0 3
  • 原文链接 攻破JAVA NIO技术壁垒 现在使用NIO的场景越来越多,很多网上的技术框架或多或少的使用NIO技...
    阳光的技术小栈阅读 498评论 2 3
  • 干画法 宝虹中粗300g 1.线稿 2.浅色用中黄色,等干了之后将橘黄色叠加。 3.高光地方留白,深色部分用大红等...
    可爱的小猪琳琳阅读 672评论 0 4