程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java NIO類庫Selector機制解析

Java NIO類庫Selector機制解析

編輯:關於JAVA

一、 前言

自從 J2SE 1.4 版本以來, JDK 發布了全新的 I/O 類庫,簡稱 NIO ,其不但引入了全新的高效的 I/O 機制,同時,也引入了多路復用的異步模式。 NIO 的包中主要包含了這樣幾種抽象數據類型:

Buffer :包含數據且用於讀寫的線形表結構。其中還提供了一個特殊類用於內存映射文件的 I/O 操作。

Charset :它提供 Unicode 字符串影射到字節序列以及逆映射的操作。

Channels :包含 socket , file 和 pipe 三種管道,都是全雙工的通道。

Selector :多個異步 I/O 操作集中到一個或多個線程中(可以被看成是 Unix 中 select() 函數的面向對象版本)。

我的大學同學趙锟在使用 NIO 類庫書寫相關網絡程序的時候,發現了一些 Java 異常 RuntimeException ,異常的報錯信息讓他開始了對 NIO 的 Selector 進行了一些調查。當趙锟對我共享了 Selector 的一些底層機制的猜想和調查時候,我們覺得這是一件很有意思的事情,於是在伙同趙锟進行過一系列的調查後,我倆發現了很多有趣的事情,於是導致了這篇文章的產生。這也是為什麼本文的作者署名為我們兩人的原因。

先要說明的一點是,趙锟和我本質上都是出身於 Unix/Linux/C/C++ 的開發人員,對於 Java ,這並不是我們的長處,這篇文章本質上出於對 Java 的 Selector 的好奇,因為從表面上來看 Selector 似乎做到了一些讓我們這些 C/C++ 出身的人比較驚奇的事情。

下面讓我來為你講述一下這段故事。

二、 故事開始 : 讓 C++程序員寫 Java程序 !

沒有嚴重內存問題,大量豐富的 SDK 類庫,超容易的跨平台,除了在性能上有些微辭, C++ 出身的程序員從來都不會覺得 Java 是一件很困難的事情。當然,對於長期習慣於使用操作系統 API (系統調用 System Call )的 C/C++ 程序來說,面對 Java 中的比較“另類”地操作系統資源的方法可能會略感困惑,但萬變不離其宗,只需要對面向對象的設計模式有一定的了解,用不了多長時間, Java 的 SDK 類庫也能玩得隨心所欲。

在使用 Java 進行相關網絡程序的的設計時,出身 C/C++ 的人,首先想到的框架就是多路復用,想到多路復用, Unix/Linux 下馬上就能讓從想到 select, poll, epoll 系統調用。於是,在看到 Java 的 NIO 中的 Selector 類時必然會倍感親切。稍加查閱一下 SDK 手冊以及相關例程,不一會兒,一個多路復用的框架便呈現出來,隨手做個單元測試,沒啥問題,一切和 C/C++ 照舊。然後告訴兄弟們,框架搞定,以後咱們就在 Windows 上開發及單元測試,完成後到運行環境 Unix 上集成測試。心中並暗自念到,跨平台就好啊,開發活動都可以跨平台了。

然而,好景不長,隨著代碼越來越多,邏輯越來越復雜。好好的框架居然在 Windows 上單元測試運行開始出現異常,看著 Java 運行異常出錯的函數棧,異常居然由 Selector.open() 拋出,錯誤信息居然是 Unable to establish loopback connection 。

“Selector.open() 居然報 loopback connection 錯誤,憑什麼?不應該啊? open 的時候又沒有什麼 loopback 的 socket 連接,怎麼會報這個錯? ”

長期使用 C/C++ 的程序當然會對操作系統的調用非常熟悉,雖然 Java 的虛擬機搞的什麼系統調用都不見了,但 C/C++ 的程序員必然要比 Java 程序敏感許多。

三、 開始調查 : 怎麼 Java這麼“傻” !

於是, C/C++ 的老鳥從 SystemInternals 上下載 Process Explorer 來查看一下究竟是什麼個 Loopback Connection 。 果然,打開 Java 運行進程,發現有一些自己連接自己的 localhost 的 TCP/IP 鏈接。於是另一個問題又出現了,

“ 憑什麼啊?為什麼會有自己和自己的連接?我程序裡沒有自己連接自己啊,怎麼可能會有這樣的鏈接啊?而自己連接自己的端口號居然是些奇怪的端口。 ”

問題變得越來越蹊跷了。難道這都是 Selector.open() 在做怪?難道 Selector.open() 要創建一個自己連接自己的鏈接?寫個程序看看:

import Java.nio.channels.Selector;

import Java.lang.RuntimeException;

import Java.lang.Thread;

public class TestSelector {

private static final int MAXSIZE= 5 ;

public static final void main( String argc[] ) {

Selector [] sels = new Selector[ MAXSIZE];

try {

for ( int i = 0 ;i< MAXSIZE ;++i ) {

sels[i] = Selector.open();

//sels[i].close();

}

Thread.sleep( 30000 );

} catch ( Exception ex ){

throw new RuntimeException( ex );

}

}

}

這個程序什麼也沒有,就是做 5 次 Selector.open() ,然後休息 30 秒,以便我使用 Process Explorer 工具來查看進程。程序編譯沒有問題,運行起來,在 Process Explorer 中看到下面的對話框:(居然有 10 個連接,從連接端口我們可以知道,互相連接, 如:第一個連第二個,第二個又連第一個 )

不由得贊歎我們的 Java 啊,先不說這是不是一件愚蠢的事。至少可以肯定的是, Java 在消耗寶貴的系統資源方面,已經可以趕的上某些蠕蟲病毒了。

如果不信,不妨把上面程序中的那個 MAXSIZE 的值改成 65535 試試,不一會你就會發現你的程序有這樣的錯誤了:(在我的 XP 機器上大約運行到 2000 個 Selector.open() 左右)

Exception in thread "main" java.lang.RuntimeException: Java.io.IOException: Unable to establish loopback connection

at Test.main(Test.Java:18)

Caused by: Java.io.IOException: Unable to establish loopback connection

at sun.nio.ch.PipeImpl$Initializer.run(Unknown Source)

at Java.security.AccessController.doPrivileged(Native Method)

at sun.nio.ch.PipeImpl.(Unknown Source)

at sun.nio.ch.SelectorProviderImpl.openPipe(Unknown Source)

at Java.nio.channels.Pipe.open(Unknown Source)

at sun.nio.ch.WindowsSelectorImpl.(Unknown Source)

at sun.nio.ch.WindowsSelectorProvider.openSelector(Unknown Source)

at Java.nio.channels.Selector.open(Unknown Source)

at Test.main(Test.Java:15)

Caused by: Java.Net.SocketException: No buffer space available (maximum connections reached?): connect

at sun.nio.ch.Net.connect(Native Method)

at sun.nio.ch.SocketChannelImpl.connect(Unknown Source)

at Java.nio.channels.SocketChannel.open(Unknown Source)

... 9 more

四、 繼續調查 : 如此跨平台

當然,沒人像我們這麼變態寫出那麼多的 Selector.open() ,但這正好可以讓我們來明白 Java 背著大家在干什麼事。上面的那些“愚蠢連接”是在 Windows 平台上,如果不出意外, Unix/Linux 下應該也差不多吧。

於是我們把上面的程序放在 Linux 下跑了跑。使用 netstat 命令,並沒有看到自己和自己的 Socket 連接。貌似在 Linux 上使用了和 Windows 不一樣的機制?!

如果在 Linux 上不建自己和自己的 TCP 連接的話,那麼文件描述符和端口都會被省下來了,是不是也就是說我們調用 65535 個 Selector.open() 的話,應該不會出現異常了。

可惜,在實現運行過程序當中,還是一樣報錯:(大約在 400 個 Selector.open() 左右,還不如 Windows )

Exception in thread "main" java.lang.RuntimeException: Java.io.IOException: Too many open files

at Test1.main(Test1.Java:19)

Caused by: Java.io.IOException: Too many open files

at sun.nio.ch.IOUtil.initPipe(Native Method)

at sun.nio.ch.EPollSelectorImpl.(EPollSelectorImpl.Java:49)

at sun.nio.ch.EPollSelectorProvider.openSelector(EPollSelectorProvider.Java:18)

at java.nio.channels.Selector.open(Selector.Java:209)

at Test1.main(Test1.Java:15)

我們發現,這個異常錯誤是 “Too many open files” ,於是我想到了使用 lsof 命令來查看一下打開的文件。

看到了有一些 pipe 文件,一共 5 對, 10 個(當然,管道從來都是成對的)。如下圖所示。

可見, Selector.open() 在 Linux 下不用 TCP 連接,而是用 pipe 管道。看來,這個 pipe 管道也是自己給自己的。所以,我們可以得出下面的結論:

1) Windows 下, Selector.open() 會自己和自己建立兩條 TCP 鏈接。不但消耗了兩個 TCP 連接和端口,同時也消耗了文件描述符。

2) Linux 下, Selector.open() 會自己和自己建兩條管道。同樣消耗了兩個系統的文件描述符。

估計,在 Windows 下, Sun 的 JVM 之所以選擇 TCP 連接,而不是 Pipe ,要麼是因為性能的問題,要麼是因為資源的問題。可能, Windows 下的管道的性能要慢於 TCP 鏈接,也有可能是 Windows 下的管道所消耗的資源會比 TCP 鏈接多。這些實現的細節還有待於更為深層次的挖掘。

但我們至少可以了解,原來 Java 的 Selector 在不同平台上的機制。

五、 迷惑不解 : 為什麼要自己消耗資源?

令人不解的是為什麼我們的 Java 的 New I/O 要設計成這個樣子?如果說老的 I/O 不能多路復用,如下圖所示,要開 N 多的線程去挨個偵聽每一個 Channel ( 文件描述符 ) ,如果這樣做很費資源,且效率不高的話。那為什麼在新的 I/O 機制依然需要自己連接自己,而且,還是重復連接,消耗雙倍的資源?

通過 WEB 搜索引擎沒有找到為什麼。只看到 N 多的人在報 BUG ,但 SUN 卻沒有任何解釋。

下面一個圖展示了,老的 IO 和新 IO 的在網絡編程方面的差別。看起來 NIO 的確很好很強大。但似乎比起 C/C++ 來說, Java 的這種實現會有一些不必要的開銷。

六、 它山之石 : 從 apache的 Mina框架了解 Selector

上面的調查沒過多長時間,正好同學趙锟的一個同事也在開發網絡程序,這位仁兄使用了 apache 的 Mina 框架。當我們把 Mina 框架的源碼研讀了一下後。發現在 Mina 中有這麼一個機制:

1) Mina 框架會創建一個 Work 對象的線程。

2) Work 對象的線程的 run() 方法會從一個隊列中拿出一堆 Channel ,然後使用 Selector.select() 方法來偵聽是否有數據可以讀 / 寫。

3) 最關鍵的是,在 select 的時候,如果隊列有新的 Channel 加入,那麼, Selector.select() 會被喚醒,然後重新 select 最新的 Channel 集合。

4) 要喚醒 select 方法,只需要調用 Selector 的 wakeup() 方法。

對於熟悉於系統調用的 C/C++ 程序員來說,一個阻塞在 select 上的線程有以下三種方式可以被喚醒:

1) 有數據可讀 / 寫,或出現異常。

2) 阻塞時間到,即 time out 。

3) 收到一個 non-block 的信號。可由 kill 或 pthread_kill 發出。

所以,Selector.wakeup()要喚醒阻塞的select,那麼也只能通過這三種方法,其中:

1)第二種方法可以排除,因為select一旦阻塞,應無法修改其time out時間。

2)而第三種看來只能在Linux上實現,Windows上沒有這種信號通知的機制。

所以,看來只有第一種方法了。再回想到為什麼每個Selector.open(),在Windows會建立一對自己和自己的loopback的TCP連接;在Linux上會開一對pipe(pipe在Linux下一般都是成對打開),估計我們能夠猜得出來——那就是如果想要喚醒select,只需要朝著自己的這個loopback連接發點數據過去,於是,就可以喚醒阻塞在select上的線程了。

七、 真相大白 : 可愛的Java你太不容易了

使用Linux下的strace命令,我們可以方便地證明這一點。參看下圖。圖中,請注意下面幾點:

1) 26654是主線程,之前我輸出notify the select字符串是為了做一個標記,而不至於迷失在大量的strace log中。

2) 26662是偵聽線程,也就是select阻塞的線程。

3) 圖中選中的兩行。26654的write正是wakeup()方法的系統調用,而緊接著的就是26662的epoll_wait的返回。

從上圖可見,這和我們之前的猜想正好一樣。可見,JDK的Selector自己和自己建的那些TCP連接或是pipe,正是用來實現Selector的notify和wakeup的功能的。

這兩個方法完全是來模仿Linux中的的kill和pthread_kill給阻塞在select上的線程發信號的。但因為發信號這個東西並不是一個跨平台的標准(pthread_kill這個系統調用也不是所有Unix/Linux都支持的),而pipe是所有的Unix/Linux所支持的,但Windows又不支持,所以,Windows用了TCP連接來實現這個事。

關於Windows,我一直在想,Windows的防火牆的設置是不是會讓Java的類似的程序執行異常呢?呵呵。如果不知道Java的SDK有這樣的機制,誰知道會有多少個程序為此引起的問題度過多少個不眠之夜,尤其是Java程序員。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved