4.JAVA NIO選擇器

第四章 選擇器

選擇器提供選擇執行已經就緒的任務的能力,這使得 多元 I/O 成爲可能。就像在第一章中描述的那樣,就緒選擇和多元執行使得單線程能夠有效率地同 時管理多個 I/O 通道(channels)。C/C++代碼的工具箱中,許多年前就已經有 select()和 poll()這兩個 POSIX(可移植性操作系統接口)系統調用可供使用了。 許過操作系統也提供相似的功能,但對 Java 程序員來說,就緒選擇功能直到 JDK 1.4 才成爲可行的方案。對於主要的工作經驗都是基於 Java 環境的開發的程序員來說,之前可能還沒有碰到過這種 I/O 模型。

爲了更好地說明就緒選擇,讓我們回到第三章的帶傳送通道的銀行的例子裏。想象一下,一個 有三個傳送通道的銀行。在傳統的(非選擇器)的場景裏,想象一下每個銀行的傳送通道都有一個 氣動導管,傳送到銀行裏它對應的出納員的窗口,並且每一個窗口與其他窗口是用牆壁分隔開的。 這意味着每個導管(通道)需要一個專門的出納員(工作線程)。這種方式不易於擴展,而且也是十分 浪費的。對於每個新增加的導管(通道),都需要一個新的出納員,以及其他相關的經費,如表 格、椅子、紙張的夾子(內存、CPU 週期、上下文切換)等等。並且當事情變慢下來時,這些資 源(以及相關的花費)大多數時候是閒置的。

現在想象一下另一個不同的場景,每一個氣動導管(通道)都只與一個出納員的窗口連接。這 個窗口有三個槽可以放置運輸過來的物品(數據緩衝區), 每個槽都有一個指示器(選擇鍵, selection key),當運輸的物品進入時會亮起。同時想象一下出納員(工作線程)有一個花盡量多 的時間閱讀《自己動手編寫個人檔案》一書的癖好。在每一段的最後,出納員看一眼指示燈(調用 select( )函數),來決定人一個通道是否已經就緒(就緒選擇)。在傳送帶閒置時,出納員(工作 線程)可以做其他事情,但需要注意的時候又可以進行及時的處理。

雖然這種分析並不精確,但它描述了快速檢查大量資源中的任意一個是否需要關注,而在某些 東西沒有準備好時又不必被迫等待的通用模式。這種檢查並繼續的能力是可擴展性的關鍵,它使得 僅僅使用單一的線程就可以通過就緒選擇來監控大量的通道。

選擇器及相關的類就提供了這種 API,使得我們可以在通道上進行就緒選擇。

 

1.選擇器基礎

掌握本章中討論的主題,在某種程度上,比直接理解緩衝區和通道類更困難一些。這會複雜一 些,因爲涉及了三個主要的類,它們都會同時參與到整個過程中。如果您發現自己有些困惑,記錄 下來並先看其他內容。一旦您瞭解了各個部分是如何相互適應的,以及每個部分扮演的角色,您就 會理解這些內容了。

我們會先從總體開始,然後分解爲細節。您需要將之前創建的一個或多個可選擇的通道註冊到 選擇器對象中。一個表示通道和選擇器的鍵將會被返回。選擇鍵會記住您關心的通道。它們也會追 蹤對應的通道是否已經就緒。當您調用一個選擇器對象的 select( )方法時,相關的鍵建會被更新, 用來檢查所有被註冊到該選擇器的通道。您可以獲取一個鍵的集合,從而找到當時已經就緒的通 道。通過遍歷這些鍵,您可以選擇出每個從上次您調用 select( )開始直到現在,已經就緒的通道。

這是在 3000 英尺高的地方看到的情景。現在,讓我們看看在地面上(甚至地下)到底發生了 什麼。

現在,您可能已經想要跳到例 4-1,並快速地瀏覽一下代碼了。通過在這裏和那段代碼之間的 內容,您將學到這些新類是如何工作的。在掌握了前面的段落裏的高層次的信息之後,您需要了解 選擇器模型是如何在實踐中被使用的。

從最基礎的層面來看,選擇器提供了詢問通道是否已經準備好執行每個 I/0 操作的能力。例 如,我們需要了解一個 SocketChannel 對象是否還有更多的字節需要讀取,或者我們需要知道 ServerSocketChannel 是否有需要準備接受的連接。

在與 SelectableChannel 聯合使用時,選擇器提供了這種服務,但這裏面有更多的事情需要去了 解。就緒選擇的真正價值在於潛在的大量的通道可以同時進行就緒狀態的檢查。調用者可以輕鬆地 決定多個通道中的哪一個準備好要運行。有兩種方式可以選擇:被激發的線程可以處於休眠狀態, 直到一個或者多個註冊到選擇器的通道就緒,或者它也可以週期性地輪詢選擇器,看看從上次檢查 之後, 是否有通道處於就緒狀態。 如果您考慮一下需要管理大量併發的連接的網絡服務器(web server)的實現,就可以很容易地想到如何善加利用這些能力。

乍一看,好像只要非阻塞模式就可以模擬就緒檢查功能,但實際上還不夠。非阻塞模式同時還 會執行您請求的任務,或指出它無法執行這項任務。這與檢查它是否能夠執行某種類型的操作是不 同的。舉個例子,如果您試圖執行非阻塞操作,並且也執行成功了,您將不僅僅發現 read( )是可以 執行的,同時您也已經讀入了一些數據。就下來您就需要處理這些數據了。

效率上的要求使得您不能將檢查就緒的代碼和處理數據的代碼分離開來,至少這麼做會很復 雜。

即使簡單地詢問每個通道是否已經就緒的方法是可行的,在您的代碼或一個類庫的包裏的某些代碼需要遍歷每一個候選的通道並按順序進行檢查的時候,仍然是有問題的。這會使得在檢查每個 通道是否就緒時都至少進行一次系統調用,這種代價是十分昂貴的,但是主要的問題是,這種檢查 不是原子性的。列表中的一個通道都有可能在它被檢查之後就緒,但直到下一次輪詢爲止,您並不 會覺察到這種情況。最糟糕的是,您除了不斷地遍歷列表之外將別無選擇。您無法在某個您感興趣 的通道就緒時得到通知。

這就是爲什麼傳統的監控多個 socket 的 Java 解決方案是爲每個 socket 創建一個線程並使得線 程可以在 read( )調用中阻塞,直到數據可用。這事實上將每個被阻塞的線程當作了 socket 監控器, 並將 Java 虛擬機的線程調度當作了通知機制。這兩者本來都不是爲了這種目的而設計的。程序員 和 Java 虛擬機都爲管理所有這些線程的複雜性和性能損耗付出了代價,這在線程數量的增長失控 時表現得更爲突出。

真正的就緒選擇必須由操作系統來做。操作系統的一項最重要的功能就是處理 I/O 請求並通知 各個線程它們的數據已經準備好了。選擇器類提供了這種抽象,使得 Java 代碼能夠以可移植的方 式,請求底層的操作系統提供就緒選擇服務。

讓我們看一下 java.nio.channels 包中處理就緒選擇的特定的類。

1)選擇器,可選擇通道和選擇鍵類

現在,您也許還對這些用於就緒選擇的 Java 成員感到困惑。讓我們來區分這些活動的零件並 瞭解它們是如何交互的吧。圖 4-1 的 UML 圖使得情形看起來比真實的情況更爲複雜了。看看圖 42,然後您會發現實際上只有三個有關的類 API,用於執行就緒選擇:

選擇器(Selector)

選擇器類管理着一個被註冊的通道集合的信息和它們的就緒狀態。通道是和選擇器一起被註冊 的,並且使用選擇器來更新通道的就緒狀態。當這麼做的時候,可以選擇將被激發的線程掛起,直 到有就緒的的通道。

可選擇通道(SelectableChannel)

這個抽象類提供了實現通道的可選擇性所需要的公共方法。它是所有支持就緒檢查的通道類的 父類。FileChannel 對象不是可選擇的,因爲它們沒有繼承 SelectableChannel(見圖 4-2)。 所有 socket 通道都是可選擇的,包括從管道(Pipe)對象的中獲得的通道。SelectableChannel 可以被註冊到 Selector 對象上,同時可以指定對 那個選擇器而言,那種操作是感興趣的。一個通道可以被註冊到多個選擇器上,但對每個選擇器而 言只能被註冊一次。

選擇鍵(SelectionKey)

選擇鍵封裝了特定的通道與特定的選擇器的註冊關係。選擇鍵對象被 SelectableChannel.register( ) 返回並提供一個表示這種註冊關係的標記。選擇鍵包含了 兩個比特集(以整數的形式進行編碼),指示了該註冊關係所關心的通道操作,以及通道已經準備 好的操作。

讓我們看看 SelectableChannel 的相關 API 方法

package java.nio.channels;

import java.io.IOException;
import java.nio.channels.Channel;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.spi.AbstractInterruptibleChannel;
import java.nio.channels.spi.SelectorProvider;

public abstract class SelectableChannel extends AbstractInterruptibleChannel implements Channel {

    // This is a partial API listing
    public final 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 SelectionKey keyFor(Selector sel);

    public abstract SelectorProvider provider();

    public abstract int validOps();

    public abstract SelectableChannel configureBlocking(boolean block) throws IOException;

    public abstract boolean isBlocking();

    public abstract Object blockingLock();
}

非阻塞特性與多元執行特性的關係是十分密切的——以至於 java.nio 的架構將兩者的 API 放到了一個類中。

我們已經探討了如何用上面列出的 SelecableChannel 的最後三個方法來配置並檢查通道的 阻塞模式(詳細的探討請參考 3.5.1 小節)。通道在被註冊到一個選擇器上之前,必須先設置爲非 阻塞模式(通過調用 configureBlocking(false))。

調用可選擇通道的 register( )方法會將它註冊到一個選擇器上。如果您試圖註冊一個處於阻塞 狀態的通道,register( )將拋出未檢查的 IllegalBlockingModeException 異常。此外,通道 一旦被註冊,就不能回到阻塞狀態。試圖這麼做的話,將在調用 configureBlocking( )方法時將拋出 IllegalBlockingModeException 異常。

並且, 理所當然地, 試圖註冊一個已經關閉的 SelectableChannel 實例的話, 也將拋出 ClosedChannelException 異常,就像方法原型指示的那樣。

在我們進一步瞭解 register( )和 SelectableChannel 的其他方法之前,讓我們先了解一下 Selector 類的 API,以確保我們可以更好地理解這種關係:

package java.nio.channels;

import java.io.Closeable;
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.spi.SelectorProvider;
import java.util.Set;

public abstract class Selector implements Closeable {

    public static Selector open() throws IOException

    public abstract boolean isOpen();

    public abstract void close() throws IOException;

    public abstract SelectorProvider provider();

    public abstract int select() throws IOException;

    public abstract int select(long timeout) throws IOException;

    public abstract int selectNow() throws IOException;

    public abstract Selector wakeup();

    public abstract Set<SelectionKey> keys();

    public abstract Set<SelectionKey> selectedKeys();
}

儘管 SelectableChannel 類上定義了 register( )方法,還是應該將通道註冊到選擇器上,而 不是另一種方式。選擇器維護了一個需要監控的通道的集合。一個給定的通道可以被註冊到多於一 個 的 選 擇 器 上 , 而 且 不 需 要 知 道 它 被 注 冊 了 那 個 Selector 對 象 上 。 將 register( ) 放 在 SelectableChannel 上而不是 Selector 上,這種做法看起來有點隨意。它將返回一個封裝了 兩個對象的關係的選擇鍵對象。重要的是要記住選擇器對象控制了被註冊到它之上的通道的選擇過 程。

package java.nio.channels;

public abstract class SelectionKey {

    public static final int OP_READ = 1 << 0;
    public static final int OP_WRITE = 1 << 2;
    public static final int OP_CONNECT = 1 << 3;
    public static final int OP_ACCEPT = 1 << 4;

    public abstract SelectableChannel channel();

    public abstract Selector selector();

    public abstract void cancel();

    public abstract boolean isValid();

    public abstract int interestOps();

    public abstract SelectionKey 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( )方法中構造它並將它傳遞給 所提供的選擇器對象。

在下面的章節裏,我們將瞭解關於這三個類的方法的更多細節。

2)建立選擇器

現在您可能仍然感到困惑,您在前面的三個清單中看到了大量的方法,但無法分辨出它們具體 做什麼,或者它們代表了什麼意思。在鑽研所有這一切的細節之前,讓我們看看一個經典的應用實 例。它可以幫助我們將所有東西放到一個特定的上下文中去理解。

爲了建立監控三個 Socket 通道的選擇器,您需要做像這樣的事情(參見圖 4-2):

        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 的 API 的細節:

package org.example;

import java.io.Closeable;
import java.io.IOException;
import java.nio.channels.spi.SelectorProvider;

public abstract class Selector implements Closeable {

    public static Selector open() throws IOException

    public abstract boolean isOpen();

    public abstract void close() throws IOException;

    public abstract SelectorProvider provider();
}

Selector 對象是通過調用靜態工廠方法 open( )來實例化的。選擇器不是像通道或流(stream) 那樣的基本 I/O 對象:數據從來沒有通過它們進行傳遞。類方法 open( )向 SPI 發出請求,通過默認 的 SelectorProvider 對象獲取一個新的實例。通過調用一個自定義的 SelectorProvider 對象的 openSelector( )方法來創建一個 Selector 實例也是可行的。您可以通過調用 provider( )方 法來決定由哪個 SelectorProvider 對象來創建給定的 Selector 實例。大多數情況下,您不需要關 心 SPI;只需要調用 open( )方法來創建新的 Selector 對象。在那些您必須處理它們的罕見的情況 下,您可以參考在附錄 B 中總結的通道的 SPI 包。

繼續關於將 Select 作爲 I/O 對象進行處理的話題的探討:當您不再使用它時,需要調用 close( ) 方法來釋放它可能佔用的資源並將所有相關的選擇鍵設置爲無效。一旦一個選擇器被關閉,試圖調 用它的大多數方法都將導致 ClosedSelectorException。注意 ClosedSelectorException 是一個非檢查(運行時的)錯誤。您可以通過 isOpen( )方法來測試一個選擇器是否處於被打開的狀 態。

我們將結束對 Selector 的 API 的探討,但現在先讓我們看看如何將通道註冊到選擇器上。 下面是一個之前章節中出現過的 SelectableChannel 的 API 的簡化版本:

package org.example;

import java.nio.channels.Channel;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.spi.AbstractInterruptibleChannel;

public abstract class SelectableChannel extends AbstractInterruptibleChannel implements Channel {

    // This is a partial API listing
    public final 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 SelectionKey keyFor(Selector sel);

    public abstract int validOps();
}

就像之前提到的那樣,register( )方法位於 SelectableChannel 類,儘管通道實際上是被註冊到選擇器上的。您可以看到 register( )方法接受一個 Selector 對象作爲參數,以及一個名爲 ops 的整數參數。第二個參數表示所關心的通道操作。這是一個表示選擇器在檢查通道就緒狀態時 需要關心的操作的比特掩碼。特定的操作比特值在 SelectonKey 類中被定義爲 public static 字 段。

在 JDK 1.4 中, 有四種被定義的可選擇操作:讀(read), 寫(write), 連接(connect)和接受 (accept)。

並非所有的操作都在所有的可選擇通道上被支持。例如,SocketChannel 不支持 accept。試 圖註冊不支持的操作將導致 IllegalArgumentException。您可以通過調用 validOps( )方法來 獲取特定的通道所支持的操作集合。我們可以在第三章中探討的 socket 通道類中看到這些方法。

選擇器包含了註冊到它們之上的通道的集合。在任意給定的時間裏,對於一個給定的選擇器和 一個給定的通道而言,只有一種註冊關係是有效的。但是,將一個通道註冊到多於一個的選擇器上 允許的。這麼做的話,在更新 interest 集合爲指定的值的同時,將返回與之前相同的選擇鍵。實際 上,後續的註冊都只是簡單地將與之前的註冊關係相關的鍵進行更新(見 4.2 小節)。

一個例外的情形是當您試圖將一個通道註冊到一個相關的鍵已經被取消的選擇器上,而通道仍 然處於被註冊的狀態的時候。通道不會在鍵被取消的時候立即註銷。直到下一次操作發生爲止,它 們仍然會處於被註冊的狀態(見 4.3 小節)。在這種情況下,未檢查的 CancelledKeyException 將會被拋出。請務必在鍵可能被取消的情況下檢查 SelectionKey 對象的狀態。

在之前的清單中,您可能已經注意到了 register( )的第二個版本,這個版本接受 object 參數。 這是一個方便的方法,可以傳遞您提供的對象引用,在調用新生成的選擇鍵的 attach( )方法時會將 這個對象引用返回給您。我們將會在下一節更進一步地瞭解 SelectionKey 的 API。

一個單獨的通道對象可以被註冊到多個選擇器上。可以調用 isRegistered( )方法來檢查一個通道 是否被註冊到任何一個選擇器上。這個方法沒有提供關於通道被註冊到哪個選擇器上的信息,而只 能知道它至少被註冊到了一個選擇器上。此外,在一個鍵被取消之後,直到通道被註銷爲止,可能 有時間上的延遲。這個方法只是一個提示,而不是確切的答案。

任何一個通道和選擇器的註冊關係都被封裝在一個 SelectionKey 對象中。keyFor( )方法將 返回與該通道和指定的選擇器相關的鍵。如果通道被註冊到指定的選擇器上,那麼相關的鍵將被返 回。如果它們之間沒有註冊關係,那麼將返回 null。

 

2.使用選擇鍵

讓我們看看 SelectionKey 類的 API:

package java.nio.channels;

public abstract class SelectionKey {

    public static final int OP_READ = 1 << 0;
    public static final int OP_WRITE = 1 << 2;
    public static final int OP_CONNECT = 1 << 3;
    public static final int OP_ACCEPT = 1 << 4;

    public abstract SelectableChannel channel();

    public abstract Selector selector();

    public abstract void cancel();

    public abstract boolean isValid();

    public abstract int interestOps();

    public abstract SelectionKey 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( )方法來檢查它是否仍然表示一種有效的關係。當鍵被 取消時,它將被放在相關的選擇器的已取消的鍵的集合裏。註冊不會立即被取消,但鍵會立即失效 (參見 4.3 節)。當再次調用 select( )方法時(或者一個正在進行的 select()調用結束時),已取消 的鍵的集合中的被取消的鍵將被清理掉, 並且相應的註銷也將完成。 通道會被註銷, 而新的 SelectionKey 將被返回。

當通道關閉時,所有相關的鍵會自動取消(記住,一個通道可以被註冊到多個選擇器上)。當 選擇器關閉時, 所有被註冊到該選擇器的通道都將被註銷, 並且相關的鍵將立即被無效化(取 消)。一旦鍵被無效化,調用它的與選擇相關的方法就將拋出 CancelledKeyException。

一個 SelectionKey 對象包含兩個以整數形式進行編碼的比特掩碼:一個用於指示那些通道/ 選擇器組合體所關心的操作(instrest 集合),另一個表示通道準備好要執行的操作(ready 集合)。 當前的 interest 集合可以通過調用鍵對象的 interestOps( )方法來獲取。最初,這應該是通道被註冊時 傳進來的值。這個 interset 集合永遠不會被選擇器改變,但您可以通過調用 interestOps( )方法並傳 入一個新的比特掩碼參數來改變它。interest 集合也可以通過將通道註冊到選擇器上來改變(實際 上使用一種迂迴的方式調用 interestOps( )),就像 4.1.2 小節中描的那樣。當相關的 Selector 上 的 select( )操作正在進行時改變鍵的 interest 集合,不會影響那個正在進行的選擇操作。所有更改將 會在 select( )的下一個調用中體現出來。

可以通過調用鍵的 readyOps( )方法來獲取相關的通道的已經就緒的操作。ready 集合是 interest 集合的子集,並且表示了 interest 集合中從上次調用 select( )以來已經就緒的那些操作。例如,下面 的代碼測試了與鍵關聯的通道是否就緒。如果就緒,就將數據讀取出來,寫入一個緩衝區,並將它 送到一個 consumer(消費者)方法中。

        if ((key.readyOps() & SelectionKey.OP_READ) != 0) {
            myBuffer.clear();
            key.channel().read(myBuffer);
            doSomethingWithBuffer(myBuffer.flip());
        }

就像之前提到過的那樣,有四個通道操作可以被用於測試就緒狀態。 您可以像上面的代碼那 樣,通過測試比特掩碼來檢查這些狀態,但 SelectionKey 類定義了四個便於使用的布爾方法來 爲您測試這些比特值:isReadable( ),isWritable( ),isConnectable( ), 和 isAcceptable( )。每一個方 法都與使用特定掩碼來測試 readyOps( )方法的結果的效果相同。例如:

if (key.isWritable())

等價於:

if ((key.readyOps() & SelectionKey.OP_WRITE) != 0)

這四個方法在任意一個 SelectionKey 對象上都能安全地調用。不能在一個通道上註冊一個它不 支持的操作,這種操作也永遠不會出現在 ready 集合中。調用一個不支持的操作將總是返回 false, 因爲這種操作在該通道上永遠不會準備好。

需要注意的是,通過相關的選擇鍵的 readyOps( )方法返回的就緒狀態指示只是一個提示,不是 保證。底層的通道在任何時候都會不斷改變。其他線程可能在通道上執行操作並影響它的就緒狀 態。同時,操作系統的特點也總是需要考慮的。

您可能會從 SelectionKey 的 API 中注意到儘管有獲取 ready 集合的方法,但沒有重新設置 那個集合的成員方法。事實上,您不能直接改變鍵的 ready 集合。在下一節裏,也就是描述選擇過 程時,我們將會看到選擇器和鍵是如何進行交互,以提供實時更新的就緒指示的。

讓我們試驗一下 SelectionKey 的 API 中剩下的兩個方法:

package java.nio.channels;

public abstract class SelectionKey {

    // This is a partial API listing
    public final Object attach(Object ob)

    public final Object attachment()
}

這兩個方法允許您在鍵上放置一個“附件”,並在後面獲取它。這是一種允許您將任意對象與 鍵關聯的便捷的方法。這個對象可以引用任何對您而言有意義的對象,例如業務對象、會話句柄、 其他通道等等。這將允許您遍歷與選擇器相關的鍵,使用附加在上面的對象句柄作爲引用來獲取相 關的上下文。

attach( )方法將在鍵對象中保存所提供的對象的引用。SelectionKey 類除了保存它之外,不 會將它用於任何其他用途。任何一個之前保存在鍵中的附件引用都會被替換。可以使用 null 值來清 除附件。可以通過調用 attachment( )方法來獲取與鍵關聯的附件句柄。如果沒有附件,或者顯式地通過 null 方法進行過設置,這個方法將返回 null。

 SelectableChannel 類的一個 register( )方法的重載版本接受一個 Object 類型的參數。這 是一個方便您在註冊時附加一個對象到新生成的鍵上的方法。以下代碼:

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

等價於:

SelectionKey key = channel.register (selector, SelectionKey.OP_READ); key.attach (myObject);

關於 SelectionKey 的最後一件需要注意的事情是併發性。總體上說,SelectionKey 對象 是線程安全的,但知道修改 interest 集合的操作是通過 Selector 對象進行同步的是很重要的。這 可能會導致 interestOps( )方法的調用會阻塞不確定長的一段時間。選擇器所使用的鎖策略(例如是 否在整個選擇過程中保持這些鎖)是依賴於具體實現的。幸好,這種多元處理能力被特別地設計爲 可以使用單線程來管理多個通道。被多個線程使用的選擇器也只會在系統特別複雜時產生問題。坦 白地說,如果您在多線程中共享選擇器時遇到了同步的問題,也許您需要重新思考一下您的設計。

我們已經探討了 SelectionKey 的 API,但我們還沒有談完選擇鍵的一切——遠遠沒有。讓 我們進一步瞭解如何使用選擇器管理鍵吧。

 

3.使用選擇器

既然我們已經很好地掌握了了各種不同類以及它們之間的關聯,那麼現在讓我們進一步瞭解 Selector 類,也就是就緒選擇的核心。這裏是 Selector 類的可用的 API。在 4.1.2 小節中,我們已 經看到如何創建新的選擇器,那麼那些方法還剩下:

1)選擇過程

在詳細瞭解 API 之前,您需要知道一點和 Selector 內部工作原理相關的知識。就像上面探 討的那樣, 選擇器維護着註冊過的通道的集合, 並且這些註冊關係中的任意一個都是封裝在 SelectionKey 對象中的。每一個 Selector 對象維護三個鍵的集合:

package java.nio.channels;

import java.io.Closeable;
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.util.Set;

public abstract class Selector implements Closeable {

    public abstract Set<SelectionKey> keys();

    public abstract Set<SelectionKey> selectedKeys();

    public abstract int select() throws IOException;

    public abstract int select(long timeout) throws IOException;

    public abstract int selectNow() throws IOException;

    public abstract Selector wakeup();
}

已註冊的鍵的集合(Registered key set)

與選擇器關聯的已經註冊的鍵的集合。 並不是所有註冊過的鍵都仍然有效。 這個集合通過 keys( )方法返回,並且可能是空的。這個已註冊的鍵的集合不是可以直接修改的;試圖這麼做的話 將引 java.lang.UnsupportedOperationException。

 

已選擇的鍵的集合(Selected key set)

已註冊的鍵的集合的子集。這個集合的每個成員都是相關的通道被選擇器(在前一個選擇操作 中)判斷爲已經準備好的,並且包含於鍵的 interest 集合中的操作。這個集合通過 selectedKeys( )方 法返回(並有可能是空的)。

不要將已選擇的鍵的集合與 ready 集合弄混了。這是一個鍵的集合,每個鍵都關聯一個已經準 備好至少一種操作的通道。每個鍵都有一個內嵌的 ready 集合,指示了所關聯的通道已經準備好的 操作。

鍵可以直接從這個集合中移除, 但不能添加。 試圖向已選擇的鍵的集合中添加元素將拋出 java.lang.UnsupportedOperationException。

 

已取消的鍵的集合(Cancelled key set)

已註冊的鍵的集合的子集,這個集合包含了 cancel( )方法被調用過的鍵(這個鍵已經被無效 化),但它們還沒有被註銷。這個集合是選擇器對象的私有成員,因而無法直接訪問。

在一個剛初始化的 Selector 對象中,這三個集合都是空的。

Selector 類的核心是選擇過程。這個名詞您已經在之前看過多次了——現在應該解釋一下 了。基本上來說,選擇器是對 select( )、poll( )等本地調用(native call)或者類似的操作系統特定的系 統調用的一個包裝。但是 Selector 所作的不僅僅是簡單地向本地代碼傳送參數。它對每個選擇 操作應用了特定的過程。對這個過程的理解是合理地管理鍵和它們所表示的狀態信息的基礎。

選擇操作是當三種形式的 select( )中的任意一種被調用時,由選擇器執行的。不管是哪一種形 式的調用,下面步驟將被執行:

  1. 已取消的鍵的集合將會被檢查。如果它是非空的,每個已取消的鍵的集合中的鍵將從另外兩 個集合中移除,並且相關的通道將被註銷。這個步驟結束後,已取消的鍵的集合將是空的。

  1. 已註冊的鍵的集合中的鍵的 interest 集合將被檢查。 在這個步驟中的檢查執行過後, 對 interest 集合的改動不會影響剩餘的檢查過程。

 

一旦就緒條件被定下來,底層操作系統將會進行查詢,以確定每個通道所關心的操作的真實就 緒狀態。依賴於特定的 select( )方法調用,如果沒有通道已經準備好,線程可能會在這時阻塞,通 常會有一個超時值。

直到系統調用完成爲止,這個過程可能會使得調用線程睡眠一段時間,然後當前每個通道的就 緒狀態將確定下來。對於那些還沒準備好的通道將不會執行任何的操作。對於那些操作系統指示至 少已經準備好 interest 集合中的一種操作的通道,將執行以下兩種操作中的一種:

a.如果通道的鍵還沒有處於已選擇的鍵的集合中,那麼鍵的 ready 集合將被清空,然後表示操 作系統發現的當前通道已經準備好的操作的比特掩碼將被設置。

b.否則,也就是鍵在已選擇的鍵的集合中。鍵的 ready 集合將被表示操作系統發現的當前已經 準備好的操作的比特掩碼更新。所有之前的已經不再是就緒狀態的操作不會被清除。事實上,所有 的比特位都不會被清理。由操作系統決定的 ready 集合是與之前的 ready 集合按位分離的,一旦鍵 被放置於選擇器的已選擇的鍵的集合中,它的 ready 集合將是累積的。比特位只會被設置,不會被 清理。

  1. 步驟 2 可能會花費很長時間,特別是所激發的線程處於休眠狀態時。與該選擇器相關的鍵可 能會同時被取消。當步驟 2 結束時,步驟 1 將重新執行,以完成任意一個在選擇進行的過程中,鍵 已經被取消的通道的註銷。

  1. select 操作返回的值是 ready 集合在步驟 2 中被修改的鍵的數量,而不是已選擇的鍵的集合中 的通道的總數。返回值不是已準備好的通道的總數,而是從上一個 select( )調用之後進入就緒狀態 的通道的數量。之前的調用中就緒的,並且在本次調用中仍然就緒的通道不會被計入,而那些在前 一次調用中已經就緒但已經不再處於就緒狀態的通道也不會被計入。這些通道可能仍然在已選擇的 鍵的集合中,但不會被計入返回值中。返回值可能是 0。

 

使用內部的已取消的鍵的集合來延遲註銷,是一種防止線程在取消鍵時阻塞,並防止與正在進 行的選擇操作衝突的優化。註銷通道是一個潛在的代價很高的操作,這可能需要重新分配資源(請 記住,鍵是與通道相關的,並且可能與它們相關的通道對象之間有複雜的交互)。清理已取消的 鍵,並在選擇操作之前和之後立即註銷通道,可以消除它們可能正好在選擇的過程中執行的潛在棘 手問題。這是另一個兼顧健壯性的折中方案。

Selector 類的 select( )方法有以下三種不同的形式:

這三種 select 的形式,僅僅在它們在所註冊的通道當前都沒有就緒時,是否阻塞的方面有所不 同。最簡單的沒有參數的形式可以用如下方式調用:

這種調用在沒有通道就緒時將無限阻塞。一旦至少有一個已註冊的通道就緒,選擇器的選擇鍵 就會被更新,並且每個就緒的通道的 ready 集合也將被更新。返回值將會是已經確定就緒的通道的 數目。正常情況下,這些方法將返回一個非零的值,因爲直到一個通道就緒前它都會阻塞。但是它 也可以返回非 0 值,如果選擇器的 wakeup( )方法被其他線程調用。

有時您會想要限制線程等待通道就緒的時間。這種情況下,可以使用一個接受一個超時參數的 select( )方法的重載形式:

這種調用與之前的例子完全相同,除了如果在您提供的超時時間(以毫秒計算)內沒有通道就 緒時,它將返回 0。如果一個或者多個通道在時間限制終止前就緒,鍵的狀態將會被更新,並且方 法會在那時立即返回。將超時參數指定爲 0 表示將無限期等待,那麼它就在各個方面都等同於使用 無參數版本的 select( )了。

就緒選擇的第三種也是最後一種形式是完全非阻塞的:

int n = selector.selectNow();

selectNow()方法執行就緒檢查過程,但不阻塞。如果當前沒有通道就緒,它將立即返回 0。

2)停止選擇過程

Selector 的 API 中的最後一個方法,wakeup( ),提供了使線程從被阻塞的 select( )方法中優 雅地退出的能力:

package java.nio.channels;

import java.io.Closeable;

public abstract class Selector implements Closeable {

    public abstract Selector wakeup();
}

有三種方式可以喚醒在 select( )方法中睡眠的線程:

調用 wakeup( )

調用 Selector 對象的 wakeup( )方法將使得選擇器上的第一個還沒有返回的選擇操作立即返 回。如果當前沒有在進行中的選擇,那麼下一次對 select( )方法的一種形式的調用將立即返回。後 續的選擇操作將正常進行。在選擇操作之間多次調用 wakeup( )方法與調用它一次沒有什麼不同。

有時這種延遲的喚醒行爲並不是您想要的。您可能只想喚醒一個睡眠中的線程,而使得後續的 選擇繼續正常地進行。您可以通過在調用 wakeup( )方法後調用 selectNow( )方法來繞過這個問題。 儘管如此,如果您將您的代碼構造爲合理地關注於返回值和執行選擇集合,那麼即使下一個 select( ) 方法的調用在沒有通道就緒時就立即返回,也應該不會有什麼不同。不管怎麼說,您應該爲可能發 生的事件做好準備。

調用 close( )

如果選擇器的 close( )方法被調用,那麼任何一個在選擇操作中阻塞的線程都將被喚醒,就像 wakeup( )方法被調用了一樣。與選擇器相關的通道將被註銷,而鍵將被取消。

調用 interrupt( )

如果睡眠中的線程的 interrupt( )方法被調用,它的返回狀態將被設置。如果被喚醒的線程之後 將試圖在通道上執行 I/O 操作,通道將立即關閉,然後線程將捕捉到一個異常。這是由於在第三章 中已經探討過的通道的中斷語義。使用 wakeup( )方法將會優雅地將一個在 select( )方法中睡眠的 線程喚醒。如果您想讓一個睡眠的線程在直接中斷之後繼續執行,需要執行一些步驟來清理中斷狀 態(參見 Thread.interrupted( )的相關文檔)。

Selector 對象將捕捉 InterruptedException 異常並調用 wakeup( )方法。

請注意這些方法中的任意一個都不會關閉任何一個相關的通道。中斷一個選擇器與中斷一個通 道是不一樣的(參見 3.3 節)。選擇器不會改變任意一個相關的通道,它只會檢查它們的狀態。當 一個在 select( )方法中睡眠的線程中斷時,對於通道的狀態而言,是不會產生歧義的。

 

3)管理選擇鍵

既然我們已經理解了問題的各個部分是怎樣結合在一起的,那麼是時候看看它們在正常的使用 中是如何交互的了。爲了有效地利用選擇器和鍵提供的信息,合理地管理鍵是非常重要的。

選擇是累積的。一旦一個選擇器將一個鍵添加到它的已選擇的鍵的集合中,它就不會移除這個 鍵。並且,一旦一個鍵處於已選擇的鍵的集合中,這個鍵的 ready 集合將只會被設置,而不會被清 理。乍一看,這好像會引起麻煩,因爲選擇操作可能無法表現出已註冊的通道的正確狀態。它提供 了極大的靈活性,但把合理地管理鍵以確保它們表示的狀態信息不會變得陳舊的任務交給了程序 員。

合理地使用選擇器的祕訣是理解選擇器維護的選擇鍵集合所扮演的角色。(參見 4.3.1 小節, 特別是選擇過程的第二步。)最重要的部分是當鍵已經不再在已選擇的鍵的集合中時將會發生什 麼。當通道上的至少一個感興趣的操作就緒時,鍵的 ready 集合就會被清空,並且當前已經就緒的 操作將會被添加到 ready 集合中。該鍵之後將被添加到已選擇的鍵的集合中。

清理一個 SelectKey 的 ready 集合的方式是將這個鍵從已選擇的鍵的集合中移除。選擇鍵的 就緒狀態只有在選擇器對象在選擇操作過程中才會修改。處理思想是隻有在已選擇的鍵的集合中的 鍵才被認爲是包含了合法的就緒信息的。這些信息將在鍵中長久地存在,直到鍵從已選擇的鍵的集 閤中移除,以通知選擇器您已經看到並對它進行了處理。如果下一次通道的一些感興趣的操作發生時,鍵將被重新設置以反映當時通道的狀態並再次被添加到已選擇的鍵的集合中。

這種框架提供了很多靈活性。通常的做法是在選擇器上調用一次 select 操作(這將更新已選擇的 鍵的集合),然後遍歷 selectKeys( )方法返回的鍵的集合。在按順序進行檢查每個鍵的過程中,相關 的通道也根據鍵的就緒集合進行處理。然後鍵將從已選擇的鍵的集合中被移除(通過在 Iterator 對象上調用 remove( )方法),然後檢查下一個鍵。完成後,通過再次調用 select( )方法重複這個循 環。例 4-1 中的代碼是典型的服務器的例子。

 

例 4-1. 使用 select( )來爲多個通道提供服務

package org.example;

import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

/**
 * Simple echo-back server which listens for incoming stream connections and
 * echoes back whatever it reads. A single Selector object is used to listen to
 * the server socket (to accept new connections) and all the active socket
 * channels.
 *
 * @author Ron Hitchens ([email protected])
 */
public class SelectSockets {

    public static int PORT_NUMBER = 1234;

    public static void main(String[] argv) throws Exception {
        new SelectSockets().go(argv);
    }

    public void go(String[] argv) throws Exception {
        int port = PORT_NUMBER;
        if (argv.length > 0) { // Override default listen port
            port = Integer.parseInt(argv[0]);
        }
        System.out.println("Listening on port " + port);
        // Allocate an unbound server socket channel
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        // Get the associated ServerSocket to bind it with
        ServerSocket serverSocket = serverChannel.socket();
        // Create a new Selector for use below
        Selector selector = Selector.open();
        // Set the port the server channel will listen to
        serverSocket.bind(new InetSocketAddress(port));
        // Set nonblocking mode for the listening socket
        serverChannel.configureBlocking(false);
        // Register the ServerSocketChannel with the Selector
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            // This may block for a long time. Upon returning, the
            // selected set contains keys of the ready channels.
            int n = selector.select();
            if (n == 0) {
                continue; // nothing to do
            }
            // Get an iterator over the set of selected keys
            Iterator it = selector.selectedKeys().iterator();
            // Look at each key in the selected set
            while (it.hasNext()) {
                SelectionKey key = (SelectionKey) it.next();
                // Is a new connection coming in?
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel channel = server.accept();
                    registerChannel(selector, channel, SelectionKey.OP_READ);
                    sayHello(channel);
                }
                // Is there data to read on this channel?
                if (key.isReadable()) {
                    readDataFromSocket(key);
                }
                // Remove key from selected set; it's been handled
                it.remove();
            }
        }
    }
    // ----------------------------------------------------------

    /**
     * Register the given channel with the given selector for the given
     * operations of interest
     */
    protected void registerChannel(Selector selector, SelectableChannel channel, int ops) throws Exception {
        if (channel == null) {
            return; // could happen
        }
        // Set the new channel nonblocking
        channel.configureBlocking(false);
        // Register it with the selector
        channel.register(selector, ops);
    }

    // ----------------------------------------------------------
    // Use the same byte buffer for all channels. A single thread is
    // servicing all the channels, so no danger of concurrent acccess.
    private ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

    /**
     * Sample data handler method for a channel with data ready to read.
     * <p>
     * * @param key
     * A SelectionKey object associated with a channel determined by
     * the selector to be ready for reading. If the channel returns
     * an EOF condition, it is closed here, which automatically
     * invalidates the associated key. The selector will then
     * de-register the channel on the next select call.
     */
    protected void readDataFromSocket(SelectionKey key) throws Exception {
        SocketChannel socketChannel = (SocketChannel) key.channel();
        int count;
        buffer.clear(); // Empty buffer
        // Loop while data is available; channel is nonblocking
        while ((count = socketChannel.read(buffer)) > 0) {
            buffer.flip(); // Make buffer readable
            // Send the data; don't assume it goes all at once
            while (buffer.hasRemaining()) {
                socketChannel.write(buffer);
            }
            // WARNING: the above loop is evil. Because
            // it's writing back to the same nonblocking
            // channel it read the data from, this code can
            // potentially spin in a busy loop. In real life
            // you'd do something more useful than this.
            buffer.clear(); // Empty buffer
        }
        if (count < 0) {
            // Close channel on EOF, invalidates the key
            socketChannel.close();
        }
    }
    // ----------------------------------------------------------

    /**
     * Spew a greeting to the incoming client connection.
     *
     * @param channel The newly connected SocketChannel to say hello to.
     */
    private void sayHello(SocketChannel channel) throws Exception {
        buffer.clear();
        buffer.put("Hi there!\r\n".getBytes());
        buffer.flip();
        channel.write(buffer);
    }
}

例 4-1 實現了一個簡單的服務器。它創建了 ServerSocketChannel 和 Selector 對象, 並將通道註冊到選擇器上。我們不在註冊的鍵中保存服務器 socket 的引用,因爲它永遠不會被注 銷。這個無限循環在最上面先調用了 select( ),這可能會無限期地阻塞。當選擇結束時,就遍歷選 擇鍵並檢查已經就緒的通道。

如果一個鍵指示與它相關的通道已經準備好執行一個 accecpt( )操作,我們就通過鍵獲取關聯 的通道, 並將它轉換爲 SeverSocketChannel 對象。 我們都知道這麼做是安全的, 因爲只有 ServerSocketChannel 支持 OP_ACCEPT 操作。 我們也知道我們的代碼只把對一個單一的 ServerSocketChannel 對象的 OP_ACCEPT 操作進行了註冊。通過對服務器 socket 通道的引 用, 我們調用了它的 accept( )方法, 來獲取剛到達的 socket 的句柄。 返回的對象的類型是 SocketChannel,也是一個可選擇的通道類型。這時,與創建一個新線程來從新的連接中讀取數據不同,我們只是簡單地將 socket 同多註冊到選擇器上。我們通過傳入 OP_READ 標記,告訴選擇 器我們關心新的 socket 通道什麼時候可以準備好讀取數據。

如果鍵指示通道還沒有準備好執行 accept( ),我們就檢查它是否準備好執行 read( )。任何一個 這麼指示的 socket 通道一定是之前 ServerSocketChannel 創建的 SocketChannel 對象之 一,並且被註冊爲只對讀操作感興趣。對於每個有數據需要讀取的 socket 通道,我們調用一個公共 的方法來讀取並處理這個帶有數據的 socket。需要注意的是這個公共方法需要準備好以非阻塞的方 式處理 socket 上的不完整的數據。它需要迅速地返回,以其他帶有後續輸入的通道能夠及時地得到 處理。例 4-1 中只是簡單地對數據進行響應,將數據寫回 socket,傳回給發送者。

在循環的底部,我們通過調用 Iterator(迭代器)對象的 remove()方法,將鍵從已選擇的鍵 的集合中移除。鍵可以直接從 selectKeys()返回的 Set 中移除,但同時需要用 Iterator 來檢查集 合,您需要使用迭代器的 remove()方法來避免破壞迭代器內部的狀態。

 

4)併發性

選擇器對象是線程安全的,但它們包含的鍵集合不是。通過 keys( )和 selectKeys( )返回的鍵的 集合是 Selector 對象內部的私有的 Set 對象集合的直接引用。這些集合可能在任意時間被改變。已 註冊的鍵的集合是隻讀的。如果您試圖修改它,那麼您得到的獎品將是一個 java.lang.UnsupportedOperationException, 但是當您在觀察它們的時候, 它們可能發 生了改變的話,您仍然會遇到麻煩。Iterator 對象是快速失敗的(fail-fast):如果底層的 Set 被改 變了,它們將會拋出 java.util.ConcurrentModificationException,因此如果您期望在 多個線程間共享選擇器和/或鍵,請對此做好準備。您可以直接修改選擇鍵,但請注意您這麼做時 可能會徹底破壞另一個線程的 Iterator。

如果在多個線程併發地訪問一個選擇器的鍵的集合的時候存在任何問題,您可以採取一些步驟 來合理地同步訪問。在執行選擇操作時,選擇器在 Selector 對象上進行同步,然後是已註冊的 鍵的集合,最後是已選擇的鍵的集合,按照這樣的順序。已取消的鍵的集合也在選擇過程的的第 1 步和第 3 步之間保持同步(當與已取消的鍵的集合相關的通道被註銷時)。

在多線程的場景中,如果您需要對任何一個鍵的集合進行更改,不管是直接更改還是其他操作 帶來的副作用,您都需要首先以相同的順序,在同一對象上進行同步。鎖的過程是非常重要的。如 果競爭的線程沒有以相同的順序請求鎖,就將會有死鎖的潛在隱患。如果您可以確保否其他線程不 會同時訪問選擇器,那麼就不必要進行同步了。

Selector 類的 close( )方法與 slect( )方法的同步方式是一樣的,因此也有一直阻塞的可能性。在選擇過程還在進行的過程中,所有對 close( )的調用都會被阻塞,直到選擇過程結束,或者 執行選擇的線程進入睡眠。在後面的情況下,執行選擇的線程將會在執行關閉的線程獲得鎖是立即 被喚醒,並關閉選擇器(參見 4.3.2 小節)。

 

4.異步關閉能力

任何時候都有可能關閉一個通道或者取消一個選擇鍵。除非您採取步驟進行同步,否則鍵的狀 態及相關的通道將發生意料之外的改變。一個特定的鍵的集合中的一個鍵的存在並不保證鍵仍然是 有效的,或者它相關的通道仍然是打開的。

關閉通道的過程不應該是一個耗時的操作。NIO 的設計者們特別想要阻止這樣的可能性:一個 線程在關閉一個處於選擇操作中的通道時,被阻塞於無限期的等待。當一個通道關閉時,它相關的 鍵也就都被取消了。這並不會影響正在進行的 select( ),但這意味着在您調用 select( )之前仍然是有 效的鍵,在返回時可能會變爲無效。您總是可以使用由選擇器的 selectKeys( )方法返回的已選擇的 鍵的集合:請不要自己維護鍵的集合。理解 3.4.5 小節描述的選擇過程,對於避免遇到問題而言是 非常重要的。

您可以參考 4.3.2 小節,以詳細瞭解一個在 select( )中阻塞的線程是如何被喚醒的。

如果您試圖使用一個已經失效的鍵,大多數方法將拋出 CancelledKeyException。但是, 您可以安全地從從已取消的鍵中獲取通道的句柄。如果通道已經關閉時,仍然試圖使用它的話,在 大多數情況下將引發 ClosedChannelException。

 

5.選擇過程的可擴展性

我多次提到選擇器可以簡化用單線程同時管理多個可選擇通道的實現。使用一個線程來爲多個 通道提供服務,通過消除管理各個線程的額外開銷,可能會降低複雜性並可能大幅提升性能。但只 使用一個線程來服務所有可選擇的通道是否是一個好主意呢?這要看情況。

對單 CPU 的系統而言這可能是一個好主意,因爲在任何情況下都只有一個線程能夠運行。通 過消除在線程之間進行上下文切換帶來的額外開銷,總吞吐量可以得到提高。但對於一個多 CPU 的系統呢?在一個有 n 個 CPU 的系統上,當一個單一的線程線性地輪流處理每一個線程時,可能 有 n-1 個 cpu 處於空閒狀態。

那麼讓不同道請求不同的服務類的辦法如何?想象一下,如果一個應用程序爲大量的分佈式的 傳感器記錄信息。每個傳感器在服務線程遍歷每個就緒的通道時需要等待數秒鐘。這在響應時間不 重要時是可以的。但對於高優先級的連接(如操作命令),如果只用一個線程爲所有通道提供服 務,將不得不在隊列中等待。不同的應用程序的要求也是不同的。您採用的策略會受到您嘗試解決 的問題的影響。

在第一個場景中,如果您想要將更多的線程來爲通道提供服務,請抵抗住使用多個選擇器的欲 望。在大量通道上執行就緒選擇並不會有很大的開銷,大多數工作是由底層操作系統完成的。管理 多個選擇器並隨機地將通道分派給它們當中的一個並不是這個問題的合理的解決方案。這隻會形成 這個場景的一個更小的版本。

一個更好的策略是對所有的可選擇通道使用一個選擇器,並將對就緒通道的服務委託給其他線 程。您只用一個線程監控通道的就緒狀態並使用一個協調好的工作線程池來處理共接收到的數據。 根據部署的條件,線程池的大小是可以調整的(或者它自己進行動態的調整)。對可選擇通道的管 理仍然是簡單的,而簡單的就是好的。

第二個場景中,某些通道要求比其他通道更高的響應速度,可以通過使用兩個選擇器來解決: 一個爲命令連接服務,另一個爲普通連接服務。但這種場景也可以使用與第一個場景十分相似的辦 法來解決。與將所有準備好的通道放到同一個線程池的做法不同,通道可以根據功能由不同的工作 線程來處理。它們可能可以是日誌線程池,命令/控制線程池,狀態請求線程池,等等。

例 4-2 的代碼是例 4-1 的一般性的選擇循環的擴展。它覆寫了 readDataFromSocket( )方法,並 使用線程池來爲準備好數據用於讀取的通道提供服務。與在主線程中同步地讀取數據不同,這個版 本的實現將 SelectionKey 對象傳遞給爲其服務的工作線程。

例 4-2. 使用線程池來爲通道提供服務

package org.example;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
import java.util.List;

/**
 * Specialization of the SelectSockets class which uses a thread pool to service
 * channels. The thread pool is an ad-hoc implementation quicky lashed togther
 * in a few hours for demonstration purposes. It's definitely not production
 * quality.
 *
 * @author Ron Hitchens ([email protected])
 */
public class SelectSocketsThreadPool extends SelectSockets {

    private static final int MAX_THREADS = 5;
    private ThreadPool pool = new ThreadPool(MAX_THREADS);
    // -------------------------------------------------------------

    public static void main(String[] argv) throws Exception {
        new SelectSocketsThreadPool().go(argv);
    }
    // -------------------------------------------------------------

    /**
     * Sample data handler method for a channel with data ready to read. This
     * method is invoked from the go( ) method in the parent class. This handler
     * delegates to a worker thread in a thread pool to service the channel,
     * then returns immediately.
     *
     * @param key A SelectionKey object representing a channel determined by the
     *            selector to be ready for reading. If the channel returns an
     *            EOF condition, it is closed here, which automatically
     *            invalidates the associated key. The selector will then
     *            de-register the channel on the next select call.
     */
    protected void readDataFromSocket(SelectionKey key) throws Exception {
        WorkerThread worker = pool.getWorker();
        if (worker == null) {
            // No threads available. Do nothing. The selection
            // loop will keep calling this method until a
            // thread becomes available. This design could
            // be improved.
            return;
        }
        // Invoking this wakes up the worker thread, then returns
        worker.serviceChannel(key);
    }
    // ---------------------------------------------------------------

    /**
     * A very simple thread pool class. The pool size is set at construction
     * time and remains fixed. Threads are cycled through a FIFO idle queue.
     */
    private class ThreadPool {

        List idle = new LinkedList();

        ThreadPool(int poolSize) {
            // Fill up the pool with worker threads
            for (int i = 0; i < poolSize; i++) {
                WorkerThread thread = new WorkerThread(this);
                // Set thread name for debugging. Start it.
                thread.setName("Worker" + (i + 1));
                thread.start();
                idle.add(thread);
            }
        }

        /**
         * Find an idle worker thread, if any. Could return null.
         */
        WorkerThread getWorker() {
            WorkerThread worker = null;
            synchronized (idle) {
                if (idle.size() > 0) {
                    worker = (WorkerThread) idle.remove(0);
                }
            }
            return (worker);
        }

        /**
         * Called by the worker thread to return itself to the idle pool.
         */
        void returnWorker(WorkerThread worker) {
            synchronized (idle) {
                idle.add(worker);
            }
        }
    }

    /**
     * A worker thread class which can drain channels and echo-back the input.
     * Each instance is constructed with a reference to the owning thread pool
     * object. When started, the thread loops forever waiting to be awakened to
     * service the channel associated with a SelectionKey object. The worker is
     * tasked by calling its serviceChannel( ) method with a SelectionKey
     * object. The serviceChannel( ) method stores the key reference in the
     * thread object then calls notify( ) to wake it up. When the channel has
     * been drained, the worker thread returns itself to its parent pool.
     */
    private class WorkerThread extends Thread {

        private ByteBuffer buffer = ByteBuffer.allocate(1024);
        private ThreadPool pool;
        private SelectionKey key;

        WorkerThread(ThreadPool pool) {
            this.pool = pool;
        }

        // Loop forever waiting for work to do
        public synchronized void run() {
            System.out.println(this.getName() + " is ready");
            while (true) {
                try {
                    // Sleep and release object lock
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    // Clear interrupt status
                    this.interrupted();
                }
                if (key == null) {
                    continue; // just in case
                }
                System.out.println(this.getName() + " has been awakened");
                try {
                    drainChannel(key);
                } catch (Exception e) {
                    System.out.println("Caught '" + e + "' closing channel");
                    // Close channel and nudge selector
                    try {
                        key.channel().close();
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                    key.selector().wakeup();
                }
                key = null;
                // Done. Ready for more. Return to pool
                this.pool.returnWorker(this);
            }
        }

        /**
         * Called to initiate a unit of work by this worker thread on the
         * provided SelectionKey object. This method is synchronized, as is the
         * run( ) method, so only one key can be serviced at a given time.
         * Before waking the worker thread, and before returning to the main
         * selection loop, this key's interest set is updated to remove OP_READ.
         * This will cause the selector to ignore read-readiness for this
         * channel while the worker thread is servicing it.
         */
        synchronized void serviceChannel(SelectionKey key) {
            this.key = key;
            key.interestOps(key.interestOps() & (~SelectionKey.OP_READ));
            this.notify(); // Awaken the thread
        }

        /**
         * The actual code which drains the channel associated with the given
         * key. This method assumes the key has been modified prior to
         * invocation to turn off selection interest in OP_READ. When this
         * method completes it re-enables OP_READ and calls wakeup( ) on the
         * selector so the selector will resume watching this channel.
         */
        void drainChannel(SelectionKey key) throws Exception {
            SocketChannel channel = (SocketChannel) key.channel();
            int count;
            buffer.clear(); // Empty buffer
            // Loop while data is available; channel is nonblocking
            while ((count = channel.read(buffer)) > 0) {
                buffer.flip(); // make buffer readable
                // Send the data; may not go all at once
                while (buffer.hasRemaining()) {
                    channel.write(buffer);
                }
                // WARNING: the above loop is evil.
                // See comments in superclass.
                buffer.clear(); // Empty buffer
            }
            if (count < 0) {
                // Close channel on EOF; invalidates the key
                channel.close();
                return;
            }
            // Resume interest in OP_READ
            key.interestOps(key.interestOps() | SelectionKey.OP_READ);
            // Cycle the selector so this key is active again
            key.selector().wakeup();
        }
    }
}

由於執行選擇過程的線程將重新循環並幾乎立即再次調用 select( ),鍵的 interest 集合將被修 改,並將 interest(感興趣的操作)從讀取就緒(read-rreadiness)狀態中移除。這將防止選擇器重複地 調用 readDataFromSocket( )(因爲通道仍然會準備好讀取數據, 直到工作線程從它那裏讀取數 據)。當工作線程結束爲通道提供的服務時,它將再次更新鍵的 ready 集合,來將 interest 重新放到 讀取就緒集合中。它也會在選擇器上顯式地嗲用 wakeup( )。如果主線程在 select( )中被阻塞,這將 使它繼續執行。這個選擇循環會再次執行一個輪迴(可能什麼也沒做)並帶着被更新的鍵重新進入 select( )。

 

總結

在本章中,我們介紹了 NIO 最強大的一面。就緒選擇對大規模、高容量的服務器端應用程序 來說是非常必要的。將這種能力補充到 Java 平臺中,意味着企業級 Java 應用程序可以和用其他編 程語言編寫的有可比性的應用程序一較高下了。本章中的關鍵概念如下:

就緒選擇相關類(Selector classes)

Selector,SelectableChannel 和 SelectionKey 這三個類組成了使得在 Java 平臺上 進行就緒檢查變得可行的三駕馬車。在 4.1 小節,我們看到了這些類相互之間的關聯以及它們表示 的含義。

選擇鍵(Selection keys)

在 4.2 小節中,我們學到了更多關於選擇鍵的知識以及如何使用它們。SelectionKey 類封 裝了 SelectableChannel 對象和 Selector 之間的關係。

選擇器(Selectors)

選擇器請求操作系統決定那個註冊到給定選擇器上的通道已經準備好指定感興趣的 I/O 操作。 我們在 4.3 小節學習了怎樣管理鍵集合並從 select( )的調用中返回。我們也探討了一些與就緒選擇 相關的併發性問題。

異步關閉能力(Asynchronous closability)

在 4.1 小節中我們接觸了關於異步關閉選擇器和通道的問題。

多線程(Multithreading)

在 4.5 小節,我們探討了怎樣將多線程用於爲可選擇通道提供服務,而不必藉助多個選擇器對 象來實現。

選擇器爲 Java 服務器應用程序提供了強有力的保證。隨着這種新的強大能力整合到商業服務 器應用程序中去,服務器端的應用程序將更加可擴展,更可靠,並且響應速度更快。

好了。我們已將結束了了解 java.nio 和它的子類的旅程。當請不要放下攝像機離開。我們 還有更多不額外收費的獎品。重新登上公交車時請注意您的腳,下一站是正則表達式的迷人大陸。

 

摘自JAVA NIO(中文版)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章