Selector提供選擇執行已經就緒的任務的能力,使得多元 I/O 成爲可能,就緒選擇和多元執行使得單線程可以有效率地同時管理多個 I/O channel。java
C/C++許多年前就已經有 select()和 poll()這兩個POSIX(可移植性操做系統接口)系統調用可供使用。許多os也提供類似的功能,但對Java 程序員來講,就緒選擇功能直到 JDK 1.4 才成爲可行方案。程序員
獲取到SocketChannel
後,直接包裝成一個任務,提交給線程池。 引入Selector後, 須要將以前建立的一或多個可選擇的Channel註冊到Selector對象,一個鍵(SelectionKey
)將會被返回。 SelectionKey
會記住你關心的Channel,也會追蹤對應的Channel是否已就緒。 安全
每一個Channel在註冊到Selector時,都有一個感興趣的操做。服務器
新的selector是經過調用系統默認的SelectorProvider對象的openSelector方法而建立的。 markdown
Selector.open()
不是單例模式的,每次調用該靜態方法,會返回新的Selector實例。網絡
可經過 Selector 被多路複用的channel。多線程
爲了與一個 selector 被使用,這個類的一個實例必須首先經由register方法。 該方法返回一個新SelectionKey表示與所述選擇channel的註冊對象。併發
一旦與一個Selector註冊,直到它的channel殘存部分註銷。 這包括被分配到由選擇的channel任何資源解除分配。socket
channel不能被直接註銷; 相反,表明其註冊的鍵必須取消。 取消鍵請求信道選擇器的下一個選擇操做期間註銷。 一鍵能夠明確地經過調用其取消cancel方法。 當channel被關閉時,全部的channel的key被隱式關閉,不管是經過調用其close方法或經過中斷一個線程阻塞於所述channel的I / O操做。 若是選擇器自己被關閉,則通道將被註銷,以及表示其註冊的鍵將被無效,而無需進一步的延遲。ide
雖說一個通道能夠被註冊到多個選擇器上,但對每一個選擇器而言只能被註冊一次
不管是否channel與一個或多個選擇可能經過調用來肯定註冊isRegistered方法。 可選擇的channel是由多個併發線程安全使用。
可選擇的信道或者是在阻斷模式或非阻塞模式。 在阻塞模式中,每個I / O操做在所述信道調用將阻塞,直到它完成。 在非阻塞模式的I / O操做不會阻塞,而且能夠傳送比被要求或全部可能沒有字節更少的字節。 可選擇信道的阻塞模式可經過調用其來肯定isBlocking方法。 新建立的可選擇通道老是處於阻塞模式。 非阻塞模式是在與基於選擇複用相結合最有用的。 信道必須被放置到非阻塞模式與一個選擇器註冊以前,而且能夠不被返回到直到它已被註銷阻塞模式。
Selector(選擇器)是Java NIO中可以檢測一到多個NIO通道,並可以知曉通道是否爲諸如讀寫事件作好準備的組件。這樣,一個單獨的線程能夠管理多個channel,從而管理多個網絡鏈接。
Selector容許單線程處理多個Channel。使用Selector,首先得向Selector註冊 Channel,而後調用它的select()。該方法會一直阻塞,直到某個註冊的Channel有事件就緒。一旦這個方法返回,線程就能夠處理這些事件,事件的例子如新鏈接 進來,數據接收等。
單線程處理多Channel的好處: 只需更少線程處理channel。事實上就是能夠只用一個線程處理全部Channel。對於os,線程間上下文切換開銷很大且每一個線程都要佔用系統資源。所以,使用線程越少越好。
但現代os和CPU在多任務方面表現的愈來愈好,多線程開銷變得愈來愈小。實際上,若CPU是多核的,不使用多任務可能就是在浪費CPU性能。使用Selector可以處理多個通道就足夠了。
經過調用Selector.open()方法建立一個Selector,以下:
爲了將Channel和Selector搭配使用,必須將channel註冊到selector。 經過SelectableChannel.register()
:
// 必須是非阻塞模式
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
複製代碼
configureBlocking()
用於設置通道的阻塞模式,該方法會調用implConfigureBlocking
implConfigureBlocking
會更改阻塞模式爲新傳入的值,默認爲true,傳入false,那麼該通道將調整爲非阻塞。而NIO最大優點就是非阻塞模型,因此通常都須要設置SocketChannel.configureBlocking(false)
。 能夠經過調用isBlocking()
判斷某個socket通道當前處於何種模式。
與Selector一塊兒使用時,Channel必須處於非阻塞模式,因此不能將FileChannel和Selector一塊兒使用,由於FileChannel不能切換到非阻塞模式。而socketChannel均可以。
注意register()方法的第二個參數。這是一個「感興趣的事件集合」,意思是在經過Selector監聽Channel時,對什麼事件感興趣。可監聽四種不一樣類型事件:
一個有數據可讀的通道能夠說是「讀就緒」。
等待寫數據的通道能夠說是「寫就緒」。
通道觸發了一個事件意思是該事件已經就緒。因此,某個channel成功鏈接到另外一個服務器稱爲「鏈接就緒」。
一個server socket channel準備好接收新進入的鏈接稱爲「接收就緒」。
這四種事件用SelectionKey的四個常量來表示:
若對不止一種事件感興趣,那麼能夠用「|」操做符將常量鏈接:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
複製代碼
封裝了特定的channel與特定的Selector的註冊關係。
SelectionKey對象被SelectableChannel.register(Selector sel, int ops)返回並提供一個表示這種註冊關係的標記。
SelectionKey包含兩個比特集(以整數形式編碼):該註冊關係所關心的channel操做及channel已就緒的操做。
interestOps(int) 將此key的interest設置爲給定值。 能夠隨時調用此方法。它是否阻塞以及持續多久取決於實現。
向Selector註冊Channel時,register()方法會返回一個SelectionKey對象,包含了一些你感興趣的屬性:
interest集合
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 集合。
通道已經準備就緒的操做的集合。在一次選擇(Selection)以後,你會首先訪問這個ready set。能夠這樣訪問ready集合:
可用像檢測interest集合那樣檢測channel中什麼事件或操做已就緒。 也可以使用如下四個方法,它們都會返回一個布爾類型:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
複製代碼
從SelectionKey訪問Channel和Selector很簡單。以下:
可將一個對象或更多信息附到SelectionKe,就能方便識別通道。 例如,能夠附加與通道一塊兒使用的Buffer,或是包含彙集數據的某個對象。 使用方法以下:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
複製代碼
一個被附加的對象可能稍後就會被attachment獲取到。 只有一個對象能夠在一個時間被附接; 調用此方法使任何先前的附接被丟棄。 當前附接能夠經過附加空被丟棄。
還可用register()向Selector註冊Channel的時候附加對象。如:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
複製代碼
一旦向Selector註冊了一或多通道,就可調用重載的select()。這些方法返回你所感興趣的事件(如鏈接、接受、讀或寫)已經準備就緒的那些通道。即若是你對「讀就緒」通道感興趣,select()方法會返回讀事件已經就緒的那些通道。
阻塞,直到至少有一個channel在你註冊的事件上就緒
和select()同樣,只是規定了最長會阻塞timeout毫秒(參數)。
不會阻塞,無論什麼channel就緒都馬上返回(此方法執行非阻塞的選擇操做。若自從上一次選擇操做後,沒有channel可選擇,則此方法直接返回0)。
select()系列方法返回的int值表示有多少channel已就緒,即自上次調用select()方法後有多少channel變成就緒狀態。 若調用select()方法,由於有一個channel變成就緒狀態,返回了1,若再次調用select()方法,若是另外一個通道就緒了,它會再次返回1。 若對第一個就緒的channel沒有作任何操做,如今就有兩個已就緒channel。可是在每次select()方法調用之間,只有一個channel就緒了。
一旦調用select()方法,而且返回值代表有一個或更多個通道就緒了,而後能夠經過調用selector的selectedKeys()方法,訪問「已選擇鍵集(selected key set)」中的就緒通道:
Set selectedKeys = selector.selectedKeys();
複製代碼
當像Selector註冊Channel時,Channel.register()方法會返回一個SelectionKey 對象。這個對象表明了註冊到該Selector的Channel。可經過SelectionKey的selectedKeySet()方法訪問這些對象。
可遍歷該selectedKeys訪問就緒的Channel:
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
} 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不會本身從selectedKeys中移除SelectionKey實例。必須在處理完通道時本身移除。下次該通道變成就緒時,Selector會再次將其放入selectedKeys。
SelectionKey.channel()方法返回的通道須要轉型成你要處理的類型,如ServerSocketChannel或SocketChannel等。
某個線程調用 select() 後阻塞了,即便沒有channel已就緒,也有辦法讓其從select()返回。 只需經過其它線程在第一個線程調用select()方法的那個對象上調用Selector.wakeup()。阻塞在select()方法上的線程會立馬返回。
如有其它線程調用了wakeup(),但當前沒有線程阻塞在select(),下個調用select()方法的線程會當即「醒來(wake up)」。
用完Selector後調用其close()會關閉該Selector,且使註冊到該Selector上的全部SelectionKey實例無效。但channel自己並不會關閉。
打開一個Selector,將一個channel註冊到這個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();
}
}
複製代碼