前面的兩篇文章中總結了Java NIO中的兩大基礎元件Buffer和Channel的相關知識點,在NIO中都是通過Channel和Buffer的協作來讀寫資料的,在這個基礎上通過selector來協調多個channel以同時讀寫資料,本文我們就來學習一下selector。
Java NIO中引入了"selector"的概念,一個selector其實是一個Java物件,能夠通過諸如連線開啟、資料就緒等事件監控多個channel。如此在單個執行緒中就可以通過一個selector同時處理多個channel,同樣也可以同時處理多個網路連線。
本文會圍繞如下幾個方面展開:
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的基本流程。