Java NIO 學習筆記(三)----Selector

目錄:
Java NIO 學習筆記(一)----概述,Channel/Buffer
Java NIO 學習筆記(二)----彙集和分散,通道到通道
Java NIO 學習筆記(三)----Selector
Java NIO 學習筆記(四)----文件通道和網絡通道
Java NIO 學習筆記(五)----路徑、文件和管道 Path/Files/Pipe
Java NIO 學習筆記(六)----異步文件通道 AsynchronousFileChannel
Java NIO 學習筆記(七)----NIO/IO 的對比和總結html

選擇器是一個 NIO 組件,它能夠檢測一個或多個 NIO 通道,並肯定哪些通道能夠用於讀或寫了。 這樣,單個線程能夠管理多個通道,從而管理多個網絡鏈接。java

摘要:一個選擇器可對應多個通道,選擇器是經過 SelectionKey 這個關鍵對象完成對多個通道的選擇的。註冊選擇器的時候會返回此對象,調用選擇器的 selectedKeys() 方法也會返回此對象。每個 SelectionKey 都包含了一些必要信息,好比關聯的通道和選擇器,獲取到 SelectionKey 後就能夠從中取出對應通道進行操做。服務器

爲何使用選擇器?

僅使用單個線程來處理多個通道的優勢是,只須要更少的線程來處理通道。 實際上只需使用一個線程來處理全部通道。 對於操做系統而言,在線程之間切換是昂貴的,而且每一個線程也佔用操做系統中的一些資源(存儲器)。 所以,使用的線程越少越好。網絡

但請記住,現代操做系統和 CPU 在多任務處理中變得愈來愈好,所以隨着時間的推移,多線程的開銷會變得愈來愈小。 事實上,若是一個 CPU 有多個內核,你可能會因多任務而浪費 CPU 能力。 不管如何,這裏知道可使用選擇器使用單個線程處理多個通道就能夠。多線程

如下是使用 1 個 Selector 處理 3 個 Channel 的線程圖示:異步

image

使用選擇器註冊通道

首先建立一個選擇器,它是經過這種方式建立的:學習

Selector selector = Selector.open();

要使用帶選擇器的通道,必須使用選擇器來註冊通道。 這是使用關聯 Channel 對象的 register() 方法完成的,以下所示:測試

channel.configureBlocking(false); //不阻塞
SelectionKey key = channel.register(selector, SelectionKey.OP_READ); // 使用通道註冊一個選擇器

通道必須處於非阻塞模式才能與選擇器一塊兒使用。 這意味着沒法將 FileChannel 與 Selector一 起使用,由於 FileChannel 沒法切換到非阻塞模式。 套接字通道則支持。操作系統

注意 register() 方法的第二個參數。 這是一個「 interest 集合」,意味着經過 Selector 在 Channel 中監聽哪些事件。能夠收聽四種不一樣的事件:線程

  • Connect 鏈接
  • Accept 接收
  • Read 讀
  • Write 寫

一個「發起事件」的通道也被稱爲「已就緒」事件。 所以,已成功鏈接到另外一臺服務器的通道是「鏈接就緒」。 接受傳入鏈接的服務器套接字通道是「接收就緒」。 準備好要讀取的數據的通道「讀就緒」。 準備好寫入數據的通道稱爲「寫就緒」。

這四個事件由四個 SelectionKey 常量表示:

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

若是要監聽多個事件,那麼能夠用「|」位或操做符將常量鏈接起來,以下所示:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

本文後面再進一步回顧 interest 集合。

register() 方法返回的 SelectionKey 對象

正如在上一節中看到的,當使用 Selector 註冊 Channel 時,register() 方法返回一個 SelectionKey 對象。 這個 SelectionKey 對象包含一些有趣的屬性:

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

interest 集合是所選擇的感興趣的事件集合,能夠經過 SelectionKey 讀取和寫入 Interest 集合,以下所示:

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 集合進行「&」位與操做,以查明某個事件是否在 interest 集合中。

ready 集合

就緒集是通道準備好的一組操做。 將在 Selector 後訪問就緒集,能夠像這樣訪問 ready set:

int readySet = selectionKey.readOps();

可使用與上面 interest 集合相同的方式,使用位與操做進行檢測頻道已準備好的事件/操做。 可是,也可使用下面這四種方法,它們都會返回一個布爾值:

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

對應 Channel + Selector

從 SelectionKey 訪問通道和選擇器很是簡單:

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

附加對象(可選)

能夠將對象或者更多信息附加到 SelectionKey ,這是識別某個通道的便捷方式。 例如,能夠將正在使用的緩衝區與通道或其餘對象相關聯。 如下是使用方法:

// 將 theObject 對象附加到 SelectionKey 
selectionKey.attach(theObject);
// 從 SelectionKey 中取出附加的對象
Object attachedObj = selectionKey.attachment();

還能夠在 register() 方法中添加參數,在使用 Selector 註冊 Channel 時就附加對象。以下:

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

經過選擇器選擇通道

使用 Selector 註冊一個或多個通道後,能夠調用其中一個 select() 方法。 這些方法返回咱們感興趣的,已就緒的事件(鏈接,接受,讀寫)的通道。 換句話說,若是對讀就緒通道感興趣,select() 方法會返回讀事件已經就緒的那些通道

如下是 select() 方法:

  • int select() : 將一直阻塞,直到至少有一個頻道爲註冊的事件作好準備。
  • int select(long timeout) :與 select() 相同,但它會最長阻塞 timeout 毫秒。
  • int selectNow() :徹底沒有阻塞。 它會當即返回任何已準備好的通道。

select() 方法返回的 int 表示有多少通道準備好了。也就是說,自從你上次調用 select() 以來,有多少頻道已經準備好了。

若是調用 select() ,由於一個頻道已準備就緒,它會返回 1 ,再次調用 select() ,由於另一個通道已準備就緒,它會再次返回 1 。若是沒有對第一個已準備就緒的通道作任何事情,那麼如今就有 2 個準備就緒的頻道,可是在每次 select() 調用之間,只有一個通道是準備就緒的。

選擇器的 selectedKeys() 方法返回的 SelectionKey 集合

一旦調用了其中一個 select() 方法而且其返回值表示有通道已準備就緒,就能夠經過調用選擇器的 selectedKeys() 方法,由於一個選擇器能夠註冊多個通道,因此這裏返回集合。經過「已選擇鍵集(selected key set)」訪問就緒通道。 以下:

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

使用 Selector 註冊通道時,Channel 對象的 register() 方法返回 SelectionKey 對象。此對象表明了該選擇器註冊的通道。

能夠迭代 selectedKeys() 方法返回的 Set 集合來訪問就緒通道。以下:

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

Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        //  ServerSocketChannel接受了一個鏈接。
    } else if (key.isConnectable()) {
        //  與遠程服務器創建鏈接。
    } else if (key.isReadable()) {
        // 一個通道已讀就緒
    } else if (key.isWritable()) {
        // 一個通道已寫就緒
    }
    keyIterator.remove();
}

此循環迭代 Set ,對於每一個 key ,它測試 key 以肯定 key 引用的通道已準備就緒的事件。

注意選擇器不會從 Set 自己中刪除 SelectionKey 對象。 完成通道處理後,必須在每次迭代結束時的調用 keyIterator.remove() 來刪除集合中已處理過的 SelectionKey 。 下一次通道變爲「就緒」時,選擇器會再次將其添加到選擇鍵集中。

這裏 Set 中的 SelectionKey 和當時使用 Selector 註冊 Channel 返回的 SelectionKey 是同樣的,請參考上述。

調用其對象方法 selectionKey.channel();就會返回 Channel 對象,這時候咱們應該將其轉換爲具體須要使用的通道,例如 ServerSocketChannel 或 SocketChannel 等。

wakeUp() 喚醒被阻塞的線程

已調用 select() 方法的線程可能會被阻塞,這是能夠經過調用 wakeUp() 方法離開 select() 方法,即便還沒有準備好任何通道。其它線程來調用阻塞線程 Selector 對象的 select() 便可讓阻塞在 select() 方法上的線程立馬返回。

若是另外一個線程調用 wakeup() 而且當前在 select() 中沒有阻塞線程,則調用 select() 的下一個線程將當即被「喚醒」。

close() 關閉選擇器

調用選擇器的 close() 方法將關閉 Selector 並使使用此 Selector 註冊的全部 SelectionKey 實例失效。 但通道自己並不會被關閉。

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;

      // 這裏的 SelectionKey 就和註冊時候返回的 key 同樣,
      // 由於一個選擇器能夠註冊多個通道,因此這裏返回集合
    Set<SelectionKey> selectedKeys = selector.selectedKeys();
    
    Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
    while(keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        if(key.isAcceptable()) {
            //  ServerSocketChannel接受了一個鏈接。
        } else if (key.isConnectable()) {
            //  與遠程服務器創建鏈接。
        } else if (key.isReadable()) {
            // 一個通道已讀就緒
        } else if (key.isWritable()) {
            // 一個通道已寫就緒
        }
        keyIterator.remove();
    }
}

再回顧一下:

  1. Selector.open() 打開選擇器,設置通道不阻塞,調用通道的 register() 方法註冊選擇器,此方法的第二個參數是一個「 interest 集合」(Connect 、Accept 、Read 、Write )
  2. register() 方法返回一個 SelectionKey 對象,此對象包含了一些註冊信息(interest 集合,ready 集合,對應 Channel,對應 Selector,附加對象(可選)),能夠調用此對象的一些方法返回一些頗有用的信息,例如Channel channel = selectionKey.channel();返回關聯的通道。
  3. 使用 Selector 註冊一個或多個通道後,能夠調用其中一個 select() 方法來選擇通道,選擇什麼通道呢?選擇咱們註冊時候, interest 集合裏面所關注的全部通道,而後返回被選擇的已準備就緒的通道數量,若是此方法返回值不爲 0 ,表明 selector 對象裏面有包含咱們須要的通道了。
  4. 知道有就緒通道後,可使用 selector.selectedKeys() 方法獲取 SelectionKey 集合,對於集合中每個 SelectionKey 都包含了一些必要信息,好比關聯的通道和選擇器,注意一個選擇器可對應多個通道。獲取到 SelectionKey 後就能夠從中取出對應通道進行操做,這也是選擇器的做用所在,一個選擇器,操做多個通道。
相關文章
相關標籤/搜索