這篇保證你完全搞懂Java NIO的Selector事件選擇器

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時,都有一個感興趣的操做。服務器

  • ServerSocketChannel 只會在選擇器上註冊一個,其感興趣的操做只有ACCEPT,表示其只關心客戶端的鏈接請求
  • SocketChannel,一般會註冊多個,由於一個server一般會接受到多個client的請求,就有對應數量的SocketChannel。SocketChannel感興趣的操做是CONNECTREADWRITE,由於其要與server創建鏈接,也須要進行讀、寫數據。

1 Selector

1.1 API

open

  • 打開一個 selector

新的selector是經過調用系統默認的SelectorProvider對象的openSelector方法而建立的。 markdown

  • 注意到默認選擇器提供者
  • Mac下的JDK,因此咱們須要下載對應平臺下的 JDK 哦!

Selector.open()不是單例模式的,每次調用該靜態方法,會返回新的Selector實例。網絡

2 SelectableChannel

簡介

可經過 Selector 被多路複用的channel。多線程

爲了與一個 selector 被使用,這個類的一個實例必須首先經由register方法。 該方法返回一個新SelectionKey表示與所述選擇channel的註冊對象。併發

  • 使用給定的Selector註冊此channel,並返回Selectionkey

一旦與一個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,從而管理多個網絡鏈接。

3 爲何使用Selector?

Selector容許單線程處理多個Channel。使用Selector,首先得向Selector註冊 Channel,而後調用它的select()。該方法會一直阻塞,直到某個註冊的Channel有事件就緒。一旦這個方法返回,線程就能夠處理這些事件,事件的例子如新鏈接 進來,數據接收等。

單線程處理多Channel的好處: 只需更少線程處理channel。事實上就是能夠只用一個線程處理全部Channel。對於os,線程間上下文切換開銷很大且每一個線程都要佔用系統資源。所以,使用線程越少越好。

但現代os和CPU在多任務方面表現的愈來愈好,多線程開銷變得愈來愈小。實際上,若CPU是多核的,不使用多任務可能就是在浪費CPU性能。使用Selector可以處理多個通道就足夠了。

  • 單線程使用一個Selector處理3個channel示例圖

4 Selector的建立

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

5 向Selector註冊Channel

爲了將Channel和Selector搭配使用,必須將channel註冊到selector。 經過SelectableChannel.register()

// 必須是非阻塞模式
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
複製代碼

configureBlocking

configureBlocking()用於設置通道的阻塞模式,該方法會調用implConfigureBlocking implConfigureBlocking會更改阻塞模式爲新傳入的值,默認爲true,傳入false,那麼該通道將調整爲非阻塞。而NIO最大優點就是非阻塞模型,因此通常都須要設置SocketChannel.configureBlocking(false)。 能夠經過調用isBlocking()判斷某個socket通道當前處於何種模式。

與Selector一塊兒使用時,Channel必須處於非阻塞模式,因此不能將FileChannel和Selector一塊兒使用,由於FileChannel不能切換到非阻塞模式。而socketChannel均可以。

注意register()方法的第二個參數。這是一個「感興趣的事件集合」,意思是在經過Selector監聽Channel時,對什麼事件感興趣。可監聽四種不一樣類型事件:

  • Read

一個有數據可讀的通道能夠說是「讀就緒」。

  • Write

等待寫數據的通道能夠說是「寫就緒」。

  • Connect

通道觸發了一個事件意思是該事件已經就緒。因此,某個channel成功鏈接到另外一個服務器稱爲「鏈接就緒」。

  • Accept

一個server socket channel準備好接收新進入的鏈接稱爲「接收就緒」。

這四種事件用SelectionKey的四個常量來表示:

若對不止一種事件感興趣,那麼能夠用「|」操做符將常量鏈接:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
複製代碼

6 SelectionKey

SelectionKey

封裝了特定的channel與特定的Selector的註冊關係。

SelectionKey對象被SelectableChannel.register(Selector sel, int ops)返回並提供一個表示這種註冊關係的標記。

SelectionKey包含兩個比特集(以整數形式編碼):該註冊關係所關心的channel操做及channel已就緒的操做。

interestOps(int) 將此key的interest設置爲給定值。 能夠隨時調用此方法。它是否阻塞以及持續多久取決於實現。

  • 興趣set肯定下一次調用選擇器的選擇方法之一時,將測試哪些操做類別是否準備就緒。使用建立key時給定的值來初始化興趣set;之後能夠經過interestOps(int)對其進行更改。
  • 準備集標識鍵的選擇器已檢測到鍵的通道已準備就緒的操做類別。建立密鑰時,將就緒集初始化爲零;不然,將其初始化爲零。它可能稍後會在選擇操做期間由選擇器更新,但沒法直接更新。

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

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

interest集合

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 集合。

ready集合

通道已經準備就緒的操做的集合。在一次選擇(Selection)以後,你會首先訪問這個ready set。能夠這樣訪問ready集合:

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

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
複製代碼

Channel + Selector

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

附加的對象

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

selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
複製代碼
  • 附加給定對象到該key。

一個被附加的對象可能稍後就會被attachment獲取到。 只有一個對象能夠在一個時間被附接; 調用此方法使任何先前的附接被丟棄。 當前附接能夠經過附加空被丟棄。

還可用register()向Selector註冊Channel的時候附加對象。如:

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
複製代碼

經過Selector選擇通道

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

select() API

select()

阻塞,直到至少有一個channel在你註冊的事件上就緒

優雅關閉執行select()的線程

  • 使用volatile boolean變量標識線程是否中止
  • 中止線程時,須要調用中止線程的interrupt()方法,由於線程有可能在wait()或sleep(),提升中止線程的及時性
  • 處於阻塞 IO的處理,儘可能使用InterruptibleChannel來代替阻塞 IO。對於NIO,若線程處於select()阻塞狀態,這時沒法及時檢測到條件變量變化,就須要人工調用wakeup(),喚醒線程,使得其能夠檢測到條件變量。

select(long timeout)

和select()同樣,只是規定了最長會阻塞timeout毫秒(參數)。

selectNow()

不會阻塞,無論什麼channel就緒都馬上返回(此方法執行非阻塞的選擇操做。若自從上一次選擇操做後,沒有channel可選擇,則此方法直接返回0)。

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

selectedKeys()

一旦調用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等。

wakeUp()

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

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

close()

用完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();
	}
}
複製代碼
相關文章
相關標籤/搜索