Java NIO學習系列三:Selector

木瓜芒果發表於2019-07-08

  前面的兩篇文章中總結了Java NIO中的兩大基礎元件Buffer和Channel的相關知識點,在NIO中都是通過Channel和Buffer的協作來讀寫資料的,在這個基礎上通過selector來協調多個channel以同時讀寫資料,本文我們就來學習一下selector。

  Java NIO中引入了"selector"的概念,一個selector其實是一個Java物件,能夠通過諸如連線開啟、資料就緒等事件監控多個channel。如此在單個執行緒中就可以通過一個selector同時處理多個channel,同樣也可以同時處理多個網路連線。

  本文會圍繞如下幾個方面展開:

  為什麼要有selector

  建立selector及註冊channel

  SelectionKey

  從Selector中選擇Channels  

  Selector的一些其他操作

  總結

 

1. 為什麼要有selector

  利用單個執行緒處理多個channel的好處是可以減少執行緒的數量,節省開銷。實際上,可以只用一個執行緒處理所有的channels。因為執行緒間的切換是很耗費作業系統資源的一項操作,並且每個執行緒都要佔用一定作業系統資源(記憶體)。因此,執行緒數量當然是越少越好,而通過引入selector的概念可以顯著減少執行緒的數量,同時又不會減少系統處理的連線數,可以簡單理解為吞吐量。

  如下示例解釋了一個Selector處理3個Channel:

2. 建立selector及註冊channel

  通過呼叫Selector的靜態方法open()來建立一個selector物件:

Selector selector = Selector.open();

  要讓Selector處理Channel就需要先將Channel註冊到Selector中,可以通過呼叫SelectableChannel.register()方法完成:

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

  Channel必須處於非阻塞模式下才能被Selector所使用,因此FileChannel不能為Selector所用,因為它不能切換到非阻塞模式。Socket channels則支援這種工作模式,通過channel的configureBlocking()方法設定其工作模式是阻塞還是非阻塞式。

  register()的第二個引數是一個set集合(interest set),用來指代那些Selector有興趣監聽的事件。一共有四種事件型別:

  • Connect
  • Accept
  • Read
  • Write

  一個channel觸發了某一事件其實是代表著它的某些狀態已經就緒,四種事件型別分別對應如下四種情況:

  • 一個channel成功和伺服器連線上就是"connect ready",由SelectionKey.OP_CONNECT指代;
  • 一個server socket channel接受了一個連線就稱為"accept ready",由SelectionKey.OP_ACCEPT指代;
  • 一個channel中資料準備好了被讀就是"read ready",由SelectionKey.OP_READ指代;
  • 一個channel可供寫入資料就是"write ready",由SelectionKey.OP_WRITE指代;

   通過這種傳參的方式,我們可以指定selector監聽channel哪些事件,如果需要同時表示多種事件,則可以如下方式來表示:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE; 

 

3. SelectionKey

  如上一部分所示,當往Selector中註冊一個Channel時,register()方法會返回一個SelectionKey物件,其包含了如下屬性:

  • The interest set
  • The ready set
  • The Channel
  • The Selector
  • An attached object (optional)

3.1 Interest Set

  可以通過如下方法讀寫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;

  可以看到,採用的是與的方式來判斷一個指定的event是否在interestSet中

3.2 Ready Set

  這個指代channel就緒的操作的集合。在selection之後就能夠獲得一個ready set(這個selection稍後會介紹到),可以通過如下方式獲取:

int readySet = selectionKey.readyOps();

  可以通過如下方式判斷是否就緒:

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

3.3 Channel + Selector

  通過如下方式來獲取channel和selector:

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

3.4 Attaching Objects

  可以給SelectionKey附帶物件,這是一個手動標記一個channel的方式,或者是給channel附帶更多資訊的方式。你可以附帶和channel連線的Buffer或者別的物件,使用方式如下:

// 可以這樣搞
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();

// 也可以這樣搞
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

 

4. 從Selector中選擇Channels

  當你往Selector中註冊了多個channel時,你可以呼叫select()方法用以獲取感興趣且就緒的channel,該方法有如下三種過載格式:

int select()
int select(long timeout)
int selectNow()
  • select()會一直阻塞,直到有一個channel就緒;
  • select(long timeout)只會阻塞一段指定的時間(單位為ms),直到有channel就緒;
  • selectNow()不會阻塞,不管是否有channel就緒都會立即返回;

  返回的int值指代有多少channel就緒(是從上一次呼叫select()之後開始算起)。比如呼叫select(),返回1,再次呼叫select(),這時又有一個channel就緒,此時任然是返回1。

4.1 selectedKeys()

  當呼叫了一次select()方法並且返回一個int值,這時你可以通過"selected key set"來獲取這些就緒的channels:

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

  通過呼叫selectedKeys()方法,返回的是一個Set,你可以遍歷以獲取就緒的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();
}

   通過判斷其對應的事件型別來做對應的操作。

 

5. Selector的一些其他操作

5.1 wakeUp()

  執行緒呼叫Selector的select()方法之後會阻塞,在這種情況下可以通過另一個執行緒呼叫同一個Selector的wakeup()方法來將其喚醒。如果一個執行緒呼叫了Selector的wakeup方法但是當前並沒有執行緒阻塞,則下一個呼叫Selector的select()方法的執行緒則不會阻塞(還記得不,前面講到select()方法會一直阻塞)。

5.2 close()

  當使用完了Selector,需要呼叫其close()方法來釋放資源,該方法會關閉Selector並使所有相關的SelectionKey失效,但是和Selector相關的channel並不會被關閉。

5.3 一個例子

  開啟一個Selector,往其中註冊一個channel,並且一直監控:

Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
  int readyChannels = selector.selectNow();
  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();
  }
}

 

6. 總結

  本文簡單總結了Java NIO中的Selector,有了Selector,我們可以實現多路複用,通過少量的執行緒來監控大量的連線,實現更高效能的伺服器,很多現代伺服器中都採用了這一特性,比如Tomcat、netty。

  我們可以往Selector中註冊一些Channel,並且指定我們需要監聽的事件型別,然後監控這些channel,一旦獲取到就緒的事件,則可以執行下一部的操作,這就是一個Selector處理channel的基本流程。

 

相關文章