Java NIO Selector
1、選擇器基礎
1.1、選擇器、可選擇通道、選擇鍵類
選擇器(Selector):
選擇器類管理著一個被註冊的通道集合的資訊和它們的就緒狀態。通道是和選擇器一起被註冊的,並且使用選擇器來更新通道的就緒狀態。當這麼做的時候,可以選擇將被激發的執行緒掛起,直到有就緒的的通道。
可選擇通道(SelectableChannel):
這個抽象類提供了實現通道的可選擇性所需要的公共方法。它是所有支援就緒檢查的通道類的父類。FileChannel物件不是可選擇的,因為它們沒有繼承SelectableChannel。所有socket通道都是可選擇的,包括從管道(Pipe)物件的中獲得的通道。SelectableChannel可以被註冊到Selector物件上,同時可以指定對那個選擇器而言,那種操作是感興趣的。一個通道可以被註冊到多個選擇器上,但對每個選擇器而言只能被註冊一次。
選擇鍵(SelectionKey):
選擇鍵封裝了特定的通道與特定的選擇器的註冊關係。選擇鍵物件被SelectableChannel.register( ) 返回並提供一個表示這種註冊關係的標記。選擇鍵包含了兩個位元集(以整數的形式進行編碼),指示了該註冊關係所關心的通道操作,以及通道已經準備好的操作。

Selector繼承.png
public abstract class SelectableChannel extends AbstractChannel implements Channel { // This is a partial API listing public abstract SelectionKey register (Selector sel, int ops) throws ClosedChannelException; public abstract SelectionKey register (Selector sel, int ops, Object att) throws ClosedChannelException; public abstract boolean isRegistered( ); public abstract int validOps( ); public abstract void configureBlocking (boolean block) throws IOException; public abstract boolean isBlocking( ); public abstract Object blockingLock( ); }
就緒選擇相關類的關係:

就緒選擇.png
呼叫可選擇通道的register( )方法會將它註冊到一個選擇器上。如果您試圖註冊一個處於阻塞狀態的通道,register( )將丟擲未檢查的IllegalBlockingModeException異常。此外,通道一旦被註冊,就不能回到阻塞狀態。試圖這麼做的話,將在呼叫configureBlocking( )方法時將丟擲IllegalBlockingModeException異常。並且,理所當然地,試圖註冊一個已經關閉的SelectableChannel例項的話,也將丟擲ClosedChannelException異常,就像方法原型指示的那樣。
selector的API:
public abstract class Selector { public static Selector open( ) throws IOException public abstract boolean isOpen( ); public abstract void close( ) throws IOException; public abstract SelectionProvider provider( ); public abstract int select( ) throws IOException; public abstract int select (long timeout) throws IOException; public abstract int selectNow( ) throws IOException; public abstract void wakeup( ); public abstract Set keys( ); public abstract Set selectedKeys( ); }
儘管SelectableChannel類上定義了register( )方法,還是應該將通道註冊到選擇器上,而不是另一種方式。選擇器維護了一個需要監控的通道的集合。一個給定的通道可以被註冊到多於一個的選擇器上,而且不需要知道它被註冊了那個Selector物件上。將register( )放在SelectableChannel上而不是Selector上,這種做法看起來有點隨意。它將返回一個封裝了兩個物件的關係的選擇鍵物件。重要的是要記住選擇器物件控制了被註冊到它之上的通道的選擇過程。
public abstract class SelectionKey { public static final int OP_READ public static final int OP_WRITE public static final int OP_CONNECT public static final int OP_ACCEPT public abstract SelectableChannel channel( ); public abstract Selector selector( ); public abstract void cancel( ); public abstract boolean isValid( ); public abstract int interestOps( ); public abstract void interestOps (int ops); public abstract int readyOps( ); public final boolean isReadable( ) public final boolean isWritable( ) public final boolean isConnectable( ) public final boolean isAcceptable( ) public final Object attach (Object ob) public final Object attachment( ) }
對於鍵的interest(感興趣的操作)集合和ready(已經準備好的操作)集合的解釋是和特定的通道相關的。每個通道的實現,將定義它自己的選擇鍵類。在register( )方法中構造它並將它傳遞給所提供的選擇器物件。
1.2、建立選擇器
為了建立監控三個Socket通道的選擇器,您需要做像這樣的事情:
Selector selector = Selector.open( ); channel1.register (selector, SelectionKey.OP_READ); channel2.register (selector, SelectionKey.OP_WRITE); channel3.register (selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); // Wait up to 10 seconds for a channel to become ready readyCount = selector.select (10000);
這些程式碼建立了一個新的選擇器,然後將這三個(已經存在的)socket通道註冊到選擇器上,而且感興趣的操作各不相同。select( )方法在將執行緒置於睡眠狀態,直到這些剛興趣的事情中的操作中的一個發生或者10秒鐘的時間過去。
Selector物件是通過呼叫靜態工廠方法open( )來例項化的。選擇器不是像通道或流(stream)那樣的基本I/O物件:資料從來沒有通過它們進行傳遞。類方法open( )向SPI發出請求,通過預設的SelectorProvider物件獲取一個新的例項。通過呼叫一個自定義的SelectorProvider物件的openSelector( )方法來建立一個Selector例項也是可行的。您可以通過呼叫provider( )方法來決定由哪個SelectorProvider物件來建立給定的Selector例項。大多數情況下,您不需要關心SPI;只需要呼叫open( )方法來建立新的Selector物件。
繼續關於將Select作為I/O物件進行處理的話題的探討:當您不再使用它時,需要呼叫close( )方法來釋放它可能佔用的資源並將所有相關的選擇鍵設定為無效。一旦一個選擇器被關閉,試圖呼叫它的大多數方法都將導致ClosedSelectorException。注意ClosedSelectorException是一個非檢查(執行時的)錯誤。您可以通過isOpen( )方法來測試一個選擇器是否處於被開啟的狀態。
並非所有的操作都在所有的可選擇通道上被支援。例如,SocketChannel不支援accept。試圖註冊不支援的操作將導致IllegalArgumentException。您可以通過呼叫validOps( )方法來獲取特定的通道所支援的操作集合。
選擇器包含了註冊到它們之上的通道的集合。在任意給定的時間裡,對於一個給定的選擇器和一個給定的通道而言,只有一種註冊關係是有效的。但是,將一個通道註冊到多於一個的選擇器上允許的。這麼做的話,在更新interest集合為指定的值的同時,將返回與之前相同的選擇鍵。實際上,後續的註冊都只是簡單地將與之前的註冊關係相關的鍵進行更新。
任何一個通道和選擇器的註冊關係都被封裝在一個SelectionKey物件中。keyFor( )方法將返回與該通道和指定的選擇器相關的鍵。如果通道被註冊到指定的選擇器上,那麼相關的鍵將被返回。如果它們之間沒有註冊關係,那麼將返回null。
2、 使用選擇鍵
SelectionKey類的API:
public abstract class SelectionKey { public static final int OP_READ public static final int OP_WRITE public static final int OP_CONNECT public static final int OP_ACCEPT public abstract SelectableChannel channel( ); public abstract Selector selector( ); public abstract void cancel( ); public abstract boolean isValid( ); public abstract int interestOps( ); public abstract void interestOps (int ops); public abstract int readyOps( ); public final boolean isReadable( ) public final boolean isWritable( ) public final boolean isConnectable( ) public final boolean isAcceptable( ) public final Object attach (Object ob) public final Object attachment( ) }
一個鍵表示了一個特定的通道物件和一個特定的選擇器物件之間的註冊關係。您可以看到前兩個方法中反映了這種關係。channel( )方法返回與該鍵相關的SelectableChannel物件,而selector( )則返回相關的Selector物件。
當應該終結這種關係的時候,可以呼叫SelectionKey物件的cancel( )方法。可以通過呼叫isValid( )方法來檢查它是否仍然表示一種有效的關係。當鍵被取消時,它將被放在相關的選擇器的已取消的鍵的集合裡。註冊不會立即被取消,但鍵會立即失效。當再次呼叫select( )方法時(或者一個正在進行的select()呼叫結束時),已取消的鍵的集合中的被取消的鍵將被清理掉,並且相應的登出也將完成。通道會被登出,而新的SelectionKey將被返回。
當通道關閉時,所有相關的鍵會自動取消(記住,一個通道可以被註冊到多個選擇器上)。當選擇器關閉時,所有被註冊到該選擇器的通道都將被登出,並且相關的鍵將立即被無效化(取消)。一旦鍵被無效化,呼叫它的與選擇相關的方法就將丟擲CancelledKeyException。
一個SelectionKey物件包含兩個以整數形式進行編碼的位元掩碼:一個用於指示那些通道/選擇器組合體所關心的操作(instrest集合),另一個表示通道準備好要執行的操作(ready集合)。當前的interest集合可以通過呼叫鍵物件的interestOps( )方法來獲取。最初,這應該是通道被註冊時傳進來的值。這個interset集合永遠不會被選擇器改變,但您可以通過呼叫interestOps( )方法並傳入一個新的位元掩碼引數來改變它。interest集合也可以通過將通道註冊到選擇器上來改變(實際上使用一種迂迴的方式呼叫interestOps( ))。當相關的Selector上的select( )操作正在進行時改變鍵的interest集合,不會影響那個正在進行的選擇操作。所有更改將會在select( )的下一個呼叫中體現出來。
可以通過呼叫鍵的readyOps( )方法來獲取相關的通道的已經就緒的操作。ready集合是interest集合的子集,並且表示了interest集合中從上次呼叫select( )以來已經就緒的那些操作。
SelectionKey類定義了四個便於使用的布林方法來為您測試這些位元值:isReadable( ),isWritable( ),isConnectable( ), 和isAcceptable( )。每一個方法都與使用特定掩碼來測試readyOps( )方法的結果的效果相同。
需要注意的是,通過相關的選擇鍵的readyOps( )方法返回的就緒狀態指示只是一個提示,不是保證。底層的通道在任何時候都會不斷改變。其他執行緒可能在通道上執行操作並影響它的就緒狀態。同時,作業系統的特點也總是需要考慮的。
public abstract class SelectionKey { // This is a partial API listing public final Object attach (Object ob) public final Object attachment( ) }
這兩個方法允許您在鍵上放置一個“附件”,並在後面獲取它。這是一種允許您將任意物件與鍵關聯的便捷的方法。這個物件可以引用任何對您而言有意義的物件,例如業務物件、會話控制代碼、其他通道等等。這將允許您遍歷與選擇器相關的鍵,使用附加在上面的物件控制代碼作為引用來獲取相關的上下文。
attach( )方法將在鍵物件中儲存所提供的物件的引用。SelectionKey類除了儲存它之外,不會將它用於任何其他用途。任何一個之前儲存在鍵中的附件引用都會被替換。可以使用null值來清除附件。可以通過呼叫attachment( )方法來獲取與鍵關聯的附件控制代碼。
3、使用選擇器
3.1、選擇過程
每一個Selector物件維護三個鍵的集合:
public abstract class Selector { // This is a partial API listing public abstract Set keys( ); public abstract Set selectedKeys( ); public abstract int select( ) throws IOException; public abstract int select (long timeout) throws IOException; public abstract int selectNow( ) throws IOException; public abstract void wakeup( ); }
已註冊的鍵的集合(Registered key set):
與選擇器關聯的已經註冊的鍵的集合。並不是所有註冊過的鍵都仍然有效。這個集合通過keys( )方法返回,並且可能是空的。這個已註冊的鍵的集合不是可以直接修改的;試圖這麼做的話將引java.lang.UnsupportedOperationException。
已選擇的鍵的集合(Selected key set):
這個集合的每個成員都是相關的通道被選擇器(在前一個選擇操作中)判斷為已經準備好的,並且包含於鍵的interest集合中的操作。這個集合通過selectedKeys( )方法返回(並有可能是空的)。
不要將已選擇的鍵的集合與ready集合弄混了。這是一個鍵的集合,每個鍵都關聯一個已經準備好至少一種操作的通道。每個鍵都有一個內嵌的ready集合,指示了所關聯的通道已經準備好的操作。
鍵可以直接從這個集合中移除,但不能新增。試圖向已選擇的鍵的集合中新增元素將丟擲java.lang.UnsupportedOperationException。
已取消的鍵的集合(Cancelled key set)
已註冊的鍵的集合的子集,這個集合包含了cancel( )方法被呼叫過的鍵(這個鍵已經被無效化),但它們還沒有被登出。這個集合是選擇器物件的私有成員,因而無法直接訪問。
Selector類的核心是選擇過程。這個名詞您已經在之前看過多次了——現在應該解釋一下了。基本上來說,選擇器是對select( )、poll( )等本地呼叫(native call)或者類似的作業系統特定的系統呼叫的一個包裝。但是Selector所作的不僅僅是簡單地向原生代碼傳送引數。它對每個選擇操作應用了特定的過程。對這個過程的理解是合理地管理鍵和它們所表示的狀態資訊的基礎。
選擇操作是當三種形式的select( )中的任意一種被呼叫時,由選擇器執行的。不管是哪一種形式的呼叫,下面步驟將被執行:
(1)已取消的鍵的集合將會被檢查。如果它是非空的,每個已取消的鍵的集合中的鍵將從另外兩個集合中移除,並且相關的通道將被登出。這個步驟結束後,已取消的鍵的集合將是空的。
(2)已註冊的鍵的集合中的鍵的interest集合將被檢查。在這個步驟中的檢查執行過後,對interest集合的改動不會影響剩餘的檢查過程。
一旦就緒條件被定下來,底層作業系統將會進行查詢,以確定每個通道所關心的操作的真實就緒狀態。依賴於特定的select( )方法呼叫,如果沒有通道已經準備好,執行緒可能會在這時阻塞,通常會有一個超時值。
直到系統呼叫完成為止,這個過程可能會使得呼叫執行緒睡眠一段時間,然後當前每個通道的就緒狀態將確定下來。對於那些還沒準備好的通道將不會執行任何的操作。對於那些作業系統指示至少已經準備好interest集合中的一種操作的通道,將執行以下兩種操作中的一種:
(a)如果通道的鍵還沒有處於已選擇的鍵的集合中,那麼鍵的ready集合將被清空,然後表示作業系統發現的當前通道已經準備好的操作的位元掩碼將被設定。
(b)否則,也就是鍵在已選擇的鍵的集合中。鍵的ready集合將被表示作業系統發現的當前已經準備好的操作的位元掩碼更新。所有之前的已經不再是就緒狀態的操作不會被清除。事實上,所有的位元位都不會被清理。由作業系統決定的ready集合是與之前的ready集合按位分離的,一旦鍵被放置於選擇器的已選擇的鍵的集合中,它的ready集合將是累積的。位元位只會被設定,不會被清理。
(3)步驟2可能會花費很長時間,特別是所激發的執行緒處於休眠狀態時。與該選擇器相關的鍵可能會同時被取消。當步驟2結束時,步驟1將重新執行,以完成任意一個在選擇進行的過程中,鍵已經被取消的通道的登出。
(4)select操作返回的值是ready集合在步驟2中被修改的鍵的數量,而不是已選擇的鍵的集合中的通道的總數。返回值不是已準備好的通道的總數,而是從上一個select( )呼叫之後進入就緒狀態的通道的數量。之前的呼叫中就緒的,並且在本次呼叫中仍然就緒的通道不會被計入,而那些在前一次呼叫中已經就緒但已經不再處於就緒狀態的通道也不會被計入。這些通道可能仍然在已選擇的鍵的集合中,但不會被計入返回值中。返回值可能是0。
使用內部的已取消的鍵的集合來延遲登出,是一種防止執行緒在取消鍵時阻塞,並防止與正在進行的選擇操作衝突的優化。登出通道是一個潛在的代價很高的操作,這可能需要重新分配資源(請記住,鍵是與通道相關的,並且可能與它們相關的通道物件之間有複雜的互動)。清理已取消的鍵,並在選擇操作之前和之後立即登出通道,可以消除它們可能正好在選擇的過程中執行的潛在棘手問題。這是另一個兼顧健壯性的折中方案。
3.2、停止選擇過程
Selector的API中的最後一個方法,wakeup( ),提供了使執行緒從被阻塞的select( )方法中優雅地退出的能力:
public abstract class Selector { // This is a partial API listing public abstract void wakeup( ); }
有三種方式可以喚醒在select( )方法中睡眠的執行緒:
呼叫wakeup( ):
呼叫Selector物件的wakeup( )方法將使得選擇器上的第一個還沒有返回的選擇操作立即返回。如果當前沒有在進行中的選擇,那麼下一次對select( )方法的一種形式的呼叫將立即返回。後續的選擇操作將正常進行。在選擇操作之間多次呼叫wakeup( )方法與呼叫它一次沒有什麼不同。
呼叫close( ):
如果選擇器的close( )方法被呼叫,那麼任何一個在選擇操作中阻塞的執行緒都將被喚醒,就像wakeup( )方法被呼叫了一樣。與選擇器相關的通道將被登出,而鍵將被取消。
呼叫interrupt( ):
如果睡眠中的執行緒的interrupt( )方法被呼叫,它的返回狀態將被設定。如果被喚醒的執行緒之後將試圖在通道上執行I/O操作,通道將立即關閉,然後執行緒將捕捉到一個異常。這是由於在第三章中已經探討過的通道的中斷語義。使用wakeup( )方法將會優雅地將一個在select( )方法中睡眠的執行緒喚醒。如果您想讓一個睡眠的執行緒在直接中斷之後繼續執行,需要執行一些步驟來清理中斷狀態。
Selector物件將捕捉InterruptedException異常並呼叫wakeup( )方法。請注意這些方法中的任意一個都不會關閉任何一個相關的通道。中斷一個選擇器與中斷一個通道是不一樣的(參見3.3節)。選擇器不會改變任意一個相關的通道,它只會檢查它們的狀態。當一個在select( )方法中睡眠的執行緒中斷時,對於通道的狀態而言,是不會產生歧義的。
3.3、管理選擇鍵
合理地使用選擇器的祕訣是理解選擇器維護的選擇鍵集合所扮演的角色。(參見4.3.1小節,特別是選擇過程的第二步。)最重要的部分是當鍵已經不再在已選擇的鍵的集合中時將會發生什麼。當通道上的至少一個感興趣的操作就緒時,鍵的ready集合就會被清空,並且當前已經就緒的操作將會被新增到ready集合中。該鍵之後將被新增到已選擇的鍵的集合中。
清理一個SelectKey的ready集合的方式是將這個鍵從已選擇的鍵的集合中移除。選擇鍵的就緒狀態只有在選擇器物件在選擇操作過程中才會修改。處理思想是隻有在已選擇的鍵的集合中的鍵才被認為是包含了合法的就緒資訊的。這些資訊將在鍵中長久地存在,直到鍵從已選擇的鍵的集合中移除,以通知選擇器您已經看到並對它進行了處理。如果下一次通道的一些感興趣的操作發生時,鍵將被重新設定以反映當時通道的狀態並再次被新增到已選擇的鍵的集合中。
這種框架提供了很多靈活性。通常的做法是在選擇器上呼叫一次select操作(這將更新已選擇的鍵的集合),然後遍歷selectKeys( )方法返回的鍵的集合。在按順序進行檢查每個鍵的過程中,相關的通道也根據鍵的就緒集合進行處理。然後鍵將從已選擇的鍵的集合中被移除(通過在Iterator物件上呼叫remove( )方法),然後檢查下一個鍵。完成後,通過再次呼叫select( )方法重複這個迴圈。
程式碼示例:
public class NioEchoServer { private static final int SELECTOR_TIMEOUT = 1000; public void start(int port){ try { //開啟服務socket ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //開啟selector Selector selector = Selector.open(); //服務監聽port埠,配置為非阻塞模式 serverSocketChannel.socket().setReuseAddress(true); serverSocketChannel.socket().bind(new InetSocketAddress(port)); serverSocketChannel.configureBlocking(false); //將channel註冊到selector中,並監聽accept事件 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); long cnt = 1; while (true){ if(selector.select(SELECTOR_TIMEOUT) == 0){ System.out.println("wait " + (cnt ++) + "s"); continue; } Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator(); while (keyIterator.hasNext()){ SelectionKey key = keyIterator.next(); keyIterator.remove(); try { if(key.isAcceptable()){ SocketChannel client = ((ServerSocketChannel)key.channel()).accept(); client.configureBlocking(false); ByteBuffer buffer = ByteBuffer.allocate(1024); client.register(selector, SelectionKey.OP_READ, buffer); System.out.println("connect accept!!"); } if(key.isReadable()){ SocketChannel client = (SocketChannel)key.channel(); ByteBuffer buffer = (ByteBuffer)key.attachment(); buffer.compact(); int count = client.read(buffer); if(count <= -1){ client.close(); }else if(count > 0){ key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); System.out.println("read count=" + count); System.out.println("read data= " + new String(buffer.array())); } } if(key.isValid() && key.isWritable()){ ByteBuffer buffer = (ByteBuffer)key.attachment(); buffer.flip(); ((SocketChannel)key.channel()).write(buffer); if(!buffer.hasRemaining()){ key.interestOps(SelectionKey.OP_READ); } buffer.compact(); } }catch (Exception e){ ((SocketChannel)key.channel()).close(); e.printStackTrace(); } } } }catch (Exception e){ e.printStackTrace(); } } public static void main(String[] args){ try { new NioEchoServer().start(9999); }catch (Exception e){ e.printStackTrace(); } } }
3.4、併發性
選擇器物件是執行緒安全的,但它們包含的鍵集合不是。通過keys( )和selectKeys( )返回的鍵的集合是Selector物件內部的私有的Set物件集合的直接引用。這些集合可能在任意時間被改變。已註冊的鍵的集合是隻讀的。如果您試圖修改它,那麼您得到的獎品將是一個java.lang.UnsupportedOperationException,但是當您在觀察它們的時候,它們可能發生了改變的話,您仍然會遇到麻煩。Iterator物件是快速失敗的(fail-fast):如果底層的Set被改變了,它們將會丟擲java.util.ConcurrentModificationException,因此如果您期望在多個執行緒間共享選擇器和/或鍵,請對此做好準備。您可以直接修改選擇鍵,但請注意您這麼做時可能會徹底破壞另一個執行緒的Iterator。
如果在多個執行緒併發地訪問一個選擇器的鍵的集合的時候存在任何問題,您可以採取一些步驟來合理地同步訪問。在執行選擇操作時,選擇器在Selector物件上進行同步,然後是已註冊的鍵的集合,最後是已選擇的鍵的集合,按照這樣的順序。已取消的鍵的集合也在選擇過程的的第1步和第3步之間保持同步
在多執行緒的場景中,如果您需要對任何一個鍵的集合進行更改,不管是直接更改還是其他操作帶來的副作用,您都需要首先以相同的順序,在同一物件上進行同步。鎖的過程是非常重要的。如果競爭的執行緒沒有以相同的順序請求鎖,就將會有死鎖的潛在隱患。如果您可以確保否其他執行緒不會同時訪問選擇器,那麼就不必要進行同步了。
Selector類的close( )方法與slect( )方法的同步方式是一樣的,因此也有一直阻塞的可能性。在選擇過程還在進行的過程中,所有對close( )的呼叫都會被阻塞,直到選擇過程結束,或者執行選擇的執行緒進入睡眠。在後面的情況下,執行選擇的執行緒將會在執行關閉的執行緒獲得鎖是立即被喚醒,並關閉選擇器。
4、非同步關閉功能
關閉通道的過程不應該是一個耗時的操作。NIO的設計者們特別想要阻止這樣的可能性:一個執行緒在關閉一個處於選擇操作中的通道時,被阻塞於無限期的等待。當一個通道關閉時,它相關的鍵也就都被取消了。這並不會影響正在進行的select( ),但這意味著在您呼叫select( )之前仍然是有效的鍵,在返回時可能會變為無效。您總是可以使用由選擇器的selectKeys( )方法返回的已選擇的鍵的集合:請不要自己維護鍵的集合。
如果您試圖使用一個已經失效的鍵,大多數方法將丟擲CancelledKeyException。但是,您可以安全地從從已取消的鍵中獲取通道的控制代碼。如果通道已經關閉時,仍然試圖使用它的話,在大多數情況下將引發ClosedChannelException。
5、選擇過程的可擴充套件性
如果您想要將更多的執行緒來為通道提供服務,請抵抗住使用多個選擇器的慾望。在大量通道上執行就緒選擇並不會有很大的開銷,大多數工作是由底層作業系統完成的。管理多個選擇器並隨機地將通道分派給它們當中的一個並不是這個問題的合理的解決方案。這隻會形成這個場景的一個更小的版本。
一個更好的策略是對所有的可選擇通道使用一個選擇器,並將對就緒通道的服務委託給其他執行緒。您只用一個執行緒監控通道的就緒狀態並使用一個協調好的工作執行緒池來處理共接收到的資料。根據部署的條件,執行緒池的大小是可以調整的(或者它自己進行動態的調整)。對可選擇通道的管理仍然是簡單的,而簡單的就是好的。
某些通道要求比其他通道更高的響應速度,可以通過使用兩個選擇器來解決:一個為命令連線服務,另一個為普通連線服務。但這種場景也可以使用與第一個場景十分相似的辦法來解決。與將所有準備好的通道放到同一個執行緒池的做法不同,通道可以根據功能由不同的工作執行緒來處理。它們可能可以是日誌執行緒池,命令/控制執行緒池,狀態請求執行緒池,等等。