Java I/O API之性能分析
IO API的可伸縮性對Web應用有著極其重要的意義。Java 1.4版以前的API中,阻塞I/O令許多人失望。從J2SE 1.4版本開始,Java終于有了可伸縮的I/O API。本文分析并計算了新舊I/O API在可伸縮性方面的差異。一、概述
IO API的可伸縮性對Web應用有著極其重要的意義。Java 1.4版以前的API中,阻塞I/O令許多人失望。從J2SE 1.4版本開始,Java終于有了可伸縮的I/O API。本文分析并計算了新舊IO API在可伸縮性方面的差異。Java向Socket寫入數據時必須調用關聯的OutputStream的write()方法。只有當所有的數據全部寫入 時,write()方法調用才會返回。倘若發送緩沖區已滿且連接速度很低,這個調用可能需要一段時間才能完成。如果程序只使用單一的線程,其他連接就必須 等待,即使那些連接已經做好了調用write()的準備也一樣。為了解決這個問題,你必須把每一個Socket和一個線程關聯起來;采用這種方法之后,當 一個線程由于I/O相關的任務被阻塞時,另一個線程仍舊能夠運行。
盡管線程的開銷不如進程那么大,但是,考慮到底層的操作平臺,線 程和進程都屬于消耗大量資源的程序結構。每一個線程都要占用一定數量的內存,而且除此之外,多個線程還意味著線程上下文的切換,而這種切換也需要昂貴的資 源開銷。因此,Java需要一個新的API來分離Socket與線程之間過于緊密的聯系。在新的Java I/O API(java.nio.*)中,這個目標終于實現了。
本文分析和比較了用新、舊兩種I/O API編寫的簡單Web服務器。由于作為Web協議的HTTP不再象原來那樣只用于一些簡單的目的,因此這里介紹的例子只包含關鍵的功能,或者說,它們既不考慮安全因素,也不嚴格遵從協議規范。
二、用舊API編寫的HTTP服務器
首先我們來看看用舊式API編寫的HTTP服務器。這個實現只使用了一個類。main()方法首先創建了一個綁定到8080端口的ServerSocket:
public static void main() throws IOException { ServerSocket serverSocket = new ServerSocket(8080); for (int i=0; i < Integer.parseInt(args[0]); i++) { new Httpd(serverSocket); } } |
接下來,main()方法創建了一系列的Httpd對象,并用共享的ServerSocket初始化它們。在Httpd的構造函數中,我們保證每一個實 例都有一個有意義的名字,設置默認協議,然后通過調用其超類Thread的start()方法啟動服務器。此舉導致對run()方法的一次異步調用,而 run()方法包含一個無限循環。
在run()方法的無限循環中,ServerSocket的阻塞性accpet()方法被調用。 當客戶程序連接服務器的8080端口,accept()方法將返回一個Socket對象。每一個Socket關聯著一個InputStream和一個 OutputStream,兩者都要在后繼的handleRequest()方法調用中用到。這個方法將讀取客戶程序的請求,經過檢查和處理,然后把合適 的應答發送給客戶程序。如果客戶程序的請求合法,通過sendFile()方法返回客戶程序請求的文件;否則,客戶程序將收到相應的錯誤信息(調用 sendError())方法。
while (true) { ... socket = serverSocket.accept(); ... handleRequest(); ... socket.close(); } |
現在我們來分析一下這個實現。它能夠出色地完成任務嗎?答案基本上是肯定的。當然,請求分析過程還可以進一步優化,因為在性能方面 StringTokenizer的聲譽一直不佳。但這個程序至少已經關閉了TCP延遲(對于短暫的連接來說它很不合適),同時為外發的文件設置了緩沖。而 且更重要的是,所有的線程操作都相互獨立。新的連接請求由哪一個線程處理由本機的(因而也是速度較快的)accept()方法決定。除了 ServerSocket對象之外,各個線程之間不共享可能需要同步的任何其他資源。這個方案速度較快,但令人遺憾的是,它不具有很好的可伸縮性,其原因 就在于,很顯然地,線程是一種有限的資源。
三、非阻塞的HTTP服務器
下面我們來看看另一個使用非阻塞的新I/O API的方案。新的方案要比原來的方案稍微復雜一點,而且它需要各個線程的協作。它包含下面四個類:
·NIOHttpd
·Acceptor
·Connection
·ConnectionSelector
NIOHttpd的主要任務是啟動服務器。就象前面的Httpd一樣,一個服務器Socket被綁定到8080端口。兩者主要的區別在于,新版本的 服務器使用java.nio.channels.ServerSocketChannel而不是ServerSocket。在利用bind()方法顯式地 把Socket綁定到端口之前,必須先打開一個管道(Channel)。然后,main()方法實例化了一個ConnectionSelector和一個 Acceptor。這樣,每一個ConnectionSelector都可以用一個Acceptor注冊;另外,實例化Acceptor時還提供了 ServerSocketChannel。
public static void main() throws IOException { ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.socket().bind(new InetSocketAddress(8080)); ConnectionSelector cs = new ConnectionSelector(); new Acceptor(ssc, cs); } |
為了理解這兩個線程之間的交互過程,首先我們來仔細地分析一下Acceptor。Acceptor的主要任務是接受傳入的連接請求,并通過 ConnectionSelector注冊它們。Acceptor的構造函數調用了超類的start()方法;run()方法包含了必需的無限循環。在這 個循環中,一個阻塞性的accept()方法被調用,它最終將返回一個Socket對象——這個過程幾乎與Httpd的處理過程一樣,但這里使用的是 ServerSocketChannel的accept()方法,而不是ServerSocket的accept()方法。最后,以調用accept() 方法獲得的socketChannel對象為參數創建一個Connection對象,并通過ConnectionSelector的queue()方法注 冊它。
while (true) { ... socketChannel = serverSocketChannel.accept(); connectionSelector.queue(new Connection(socketChannel)); ... } |
總而言之:Acceptor只能在一個無限循環中接受連接請求和通過ConnectionSelector注冊連接。與Acceptor一 樣,ConnectionSelector也是一個線程。在構造函數中,它構造了一個隊列,并用Selector.open()方法打開了一個 java.nio.channels.Selector。Selector是整個服務器中最重要的部分之一,它使得程序能夠注冊連接,能夠獲取已經允許讀 取和寫入操作的連接的清單。
構造函數調用start()方法之后,run()方法里面的無限循環開始執行。在這個循環中,程序調用了Selector的select()方法。這個方法一直阻塞,直到已經注冊的連接之一做好了I/O操作的準備,或Selector的wakeup()方法被調用。
while (true) { ... int i = selector.select(); registerQueuedConnections(); ... // 處理連接... } |
當ConnectionSelector線程執行select()時,沒有一個Acceptor線程能夠用該Selector注冊連接,因為對應的方法是同步方法,理解這一點是很重要的。因此這里使用了隊列,必要時Acceptor線程向隊列加入連接。
public void queue(Connection connection) { synchronized (queue) { queue.add(connection); } selector.wakeup(); } |
緊接著把連接放入隊列的操作,Acceptor調用Selector的wakeup()方法。這個調用導致ConnectionSelector線程繼 續執行,從正在被阻塞的select()調用返回。由于Selector不再被阻塞,ConnectionSelector現在能夠從隊列注冊連接。在 registerQueuedConnections()方法中,其實施過程如下:
if (!queue.isEmpty()) { synchronized (queue) { while (!queue.isEmpty()) { Connection connection = (Connection)queue.remove(queue.size()-1); connection.register(selector); } } } |
posted on 2008-04-11 10:28 gembin 閱讀(478) 評論(0) 編輯 收藏 所屬分類: JavaSE