Java NIO系列教程(六) Selector

Java NIO系列教程(六) Selector

Selector(選擇器)是 Java NIO 中可以檢測一到多個 NIO 通道,並可以知曉通道是否爲諸如讀寫事件作好準備的組件。這樣,一個單獨的線程能夠管理多個 channel,從而管理多個網絡鏈接。深刻淺出NIO Socket實現機制java

1、爲何使用 Selector?

僅用單個線程來處理多個 Channels 的好處是,只須要更少的線程來處理通道。事實上,能夠只用一個線程處理全部的通道。對於操做系統來講,線程之間上下文切換的開銷很大,並且每一個線程都要佔用系統的一些資源(如內存)。所以,使用的線程越少越好。編程

可是,須要記住,現代的操做系統和 CPU 在多任務方面表現的愈來愈好,因此多線程的開銷隨着時間的推移,變得愈來愈小了。實際上,若是一個 CPU 有多個內核,不使用多任務多是在浪費 CPU 能力。無論怎麼說,關於那種設計的討論應該放在另外一篇不一樣的文章中。在這裏,只要知道使用 Selector 可以處理多個通道就足夠了。服務器

下面是單線程使用一個 Selector 處理 3 個 channel 的示例圖:網絡

2、Selector 的建立

經過調用 Selector.open() 方法建立一個 Selector,以下:多線程

Selector selector = Selector.open();

3、向 Selector 註冊通道

爲了將 Channel 和 Selector 配合使用,必須將 channel 註冊到 selector 上。經過 SelectableChannel.register() 方法來實現,以下:併發

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

與 Selector 一塊兒使用時,Channel 必須處於非阻塞模式下。這意味着不能將 FileChannel 與 Selector 一塊兒使用,由於 FileChannel 不能切換到非阻塞模式。而套接字通道均可以。操作系統

注意 register() 方法的第二個參數。這是一個「interest集合」,意思是在經過 Selector 監聽 Channel 時對什麼事件感興趣。能夠監聽四種不一樣類型的事件,這四種事件用 SelectionKey 的四個常量來表示:線程

  1. Connect 客戶端鏈接服務端事件,對應值爲 SelectionKey.OP_CONNECT(8)
  2. Accept 服務端接收客戶端鏈接事件,對應值爲 SelectionKey.OP_ACCEPT(16)
  3. Read 讀事件,對應值爲 SelectionKey.OP_READ(1)
  4. Write 寫事件,對應值爲 SelectionKey.OP_WRITE(4)

通道觸發了一個事件意思是該事件已經就緒。因此,某個 channel 成功鏈接到另外一個服務器稱爲「鏈接就緒」。一個 ServerSocketChannel 準備好接收新進入的鏈接稱爲「接收就緒」。一個有數據可讀的通道能夠說是「讀就緒」。等待寫數據的通道能夠說是「寫就緒」。設計

若是你對不止一種事件感興趣,那麼能夠用「位或」操做符將常量鏈接起來,以下:rest

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

在下面還會繼續提到 interest 集合。

4、SelectionKey

在上一小節中,當向 Selector 註冊 Channel 時,register() 方法會返回一個 SelectionKey 對象。這個對象包含了一些你感興趣的屬性:

  • interest 集合
  • ready 集合
  • Channel
  • Selector
  • 附加的對象(可選)

下面我會描述這些屬性。

(1) interest 集合

就像向 Selector 註冊通道一節中所描述的,interest 集合是你所選擇的感興趣的事件集合。能夠經過 SelectionKey 讀寫 interest 集合,像這樣:

int interestSet = selectionKey.interestOps();

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

能夠看到,用「位與」操做 interest 集合和給定的 SelectionKey 常量,能夠肯定某個肯定的事件是否在 interest 集合中。

(2) ready 集合

ready 集合是通道已經準備就緒的操做的集合。在一次選擇(Selection)以後,你會首先訪問這個 readySet。Selection將在下一小節進行解釋。能夠這樣訪問 ready 集合:

int readySet = selectionKey.readyOps();

能夠用像檢測 interest 集合那樣的方法,來檢測 channel 中什麼事件或操做已經就緒。可是,也可使用如下四個方法,它們都會返回一個布爾類型:

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

(3) Channel + Selector

從 SelectionKey 訪問 Channel 和 Selector 很簡單。以下:

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

(4) 附加的對象

能夠將一個對象或者更多信息附着到 SelectionKey 上,這樣就能方便的識別某個給定的通道。例如,能夠附加 與通道一塊兒使用的 Buffer,或是包含彙集數據的某個對象。使用方法以下:

selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();

還能夠在用 register() 方法向 Selector 註冊 Channel 的時候附加對象。如:

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

5、經過 Selector 選擇通道

一旦向 Selector 註冊了一或多個通道,就能夠調用幾個重載的 select() 方法。這些方法返回你所感興趣的事件(如鏈接、接受、讀或寫)已經準備就緒的那些通道。換句話說,若是你對「讀就緒」的通道感興趣,select() 方法會返回讀事件已經就緒的那些通道。

下面是 select() 方法:

  • select() 阻塞到至少有一個通道在你註冊的事件上就緒了。
  • int select(long timeout) 和 select() 同樣,除了最長會阻塞 timeout 毫秒(參數)。
  • int selectNow() 不會阻塞,無論什麼通道就緒都馬上返回(譯者注:此方法執行非阻塞的選擇操做。若是自從前一次選擇操做後,沒有通道變成可選擇的,則此方法直接返回零。)

select() 方法返回的 int 值表示有多少通道已經就緒。亦即,自上次調用 select()方法後有多少通道變成就緒狀態。若是調用 select() 方法,由於有一個通道變成就緒狀態,返回了 1,若再次調用 select() 方法,若是另外一個通道就緒了,它會再次返回 1。若是對第一個就緒的 channel 沒有作任何操做,如今就有兩個就緒的通道,但在每次 select() 方法調用之間,只有一個通道就緒了。

selectedKeys()

一旦調用了 select() 方法,而且返回值代表有一個或更多個通道就緒了,而後能夠經過調用 selector 的 selectedKeys()方法,訪問「已選擇鍵集(selected key set)」中的就緒通道。以下所示:

Set selectedKeys = selector.selectedKeys();

當像 Selector 註冊 Channel 時,Channel.register() 方法會返回一個 SelectionKey 對象。這個對象表明了註冊到該 Selector 的通道。能夠經過 SelectionKey 的 selectedKeySet() 方法訪問這些對象。

能夠遍歷這個已選擇的鍵集合來訪問就緒的通道。以下:

Set selectedKeys = selector.selectedKeys();
Iterator 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();
}

這個循環遍歷已選擇鍵集中的每一個鍵,並檢測各個鍵所對應的通道的就緒事件。

注意每次迭代末尾的 keyIterator.remove() 調用。Selector 不會本身從已選擇鍵集中移除 SelectionKey 實例。必須在處理完通道時本身移除。下次該通道變成就緒時,Selector 會再次將其放入已選擇鍵集中。

SelectionKey.channel() 方法返回的通道須要轉型成你要處理的類型,如 ServerSocketChannel 或 SocketChannel 等。

6、wakeUp()

某個線程調用 select() 方法後阻塞了,即便沒有通道已經就緒,也有辦法讓其從 select() 方法返回。只要讓其它線程在第一個線程調用 select() 方法的那個對象上調用 Selector.wakeup() 方法便可。阻塞在 select() 方法上的線程會立馬返回。

若是有其它線程調用了 wakeup() 方法,但當前沒有線程阻塞在 select() 方法上,下個調用 select()方法的線程會當即「醒來(wake up)」。

7、close()

用完 Selector 後調用其 close()方法會關閉該 Selector,且使註冊到該 Selector 上的全部 SelectionKey 實例無效。通道自己並不會關閉。

8、完整的示例

這裏有一個完整的示例,打開一個 Selector,註冊一個通道註冊到這個 Selector 上(通道的初始化過程略去),而後持續監控這個 Selector 的四種事件(接受,鏈接,讀,寫)是否就緒。

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 selectedKeys = selector.selectedKeys();
  Iterator 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();
  }
}

轉載自併發編程網 – ifeve.com,本文連接地址: Java NIO系列教程(六) Selector

相關文章
相關標籤/搜索