Java NIO與IO的區(qū)別和比較

Java NIO與IO的區(qū)別和比較

J2SE1.4以上版本中發(fā)布了全新的I/O類庫。本文將通過一些實例來簡單介紹NIO庫提供的一些新特性:非阻塞I/O伞矩,字符轉(zhuǎn)換笛洛,緩沖以及通道。

一. 介紹NIO

NIO包(java.nio.*)引入了四個關(guān)鍵的抽象數(shù)據(jù)類型乃坤,它們共同解決傳統(tǒng)的I/O類中的一些問題苛让。

1. Buffer:它是包含數(shù)據(jù)且用于讀寫的線形表結(jié)構(gòu)。其中還提供了一個特殊類用于內(nèi)存映射文件的I/O操作湿诊。

2. Charset:它提供Unicode字符串影射到字節(jié)序列以及逆影射的操作狱杰。

3. Channels:包含socket,file和pipe三種管道厅须,它實際上是雙向交流的通道仿畸。

4. Selector:它將多元異步I/O操作集中到一個或多個線程中(它可以被看成是Unix中select()函數(shù)或Win32中WaitForSingleEvent()函數(shù)的面向?qū)ο蟀姹荆?/p>

二. 回顧傳統(tǒng)

在介紹NIO之前,有必要了解傳統(tǒng)的I/O操作的方式朗和。以網(wǎng)絡(luò)應(yīng)用為例错沽,傳統(tǒng)方式需要監(jiān)聽一個ServerSocket,接受請求的連接為其提供服務(wù)(服務(wù)通常包括了處理請求并發(fā)送響應(yīng))圖一是服務(wù)器的生命周期圖眶拉,其中標(biāo)有粗黑線條的部分表明會發(fā)生I/O阻塞千埃。

圖一

可以分析創(chuàng)建服務(wù)器的每個具體步驟。首先創(chuàng)建ServerSocket

ServerSocket server=new ServerSocket(10000)忆植;

然后接受新的連接請求

Socket newConnection=server.accept()放可;

對于accept方法的調(diào)用將造成阻塞,直到ServerSocket接受到一個連接請求為止朝刊。一旦連接請求被接受耀里,服務(wù)器可以讀客戶socket中的請求。

InputStreamin=newConnection.getInputStream();

InputStreamReaderreader=newInputStreamReader(in);

BufferedReaderbuffer=newBufferedReader(reader);

Requestrequest=newRequest();

while(!request.isComplete()){

Stringline=buffer.readLine();

request.addLine(line);

}

這樣的操作有兩個問題拾氓,首先BufferedReader類的readLine()方法在其緩沖區(qū)未滿時會造成線程阻塞冯挎,只有一定數(shù)據(jù)填滿了緩沖區(qū)或者客戶關(guān)閉了套接字,方法才會返回痪枫。其次织堂,它回產(chǎn)生大量的垃圾叠艳,BufferedReader創(chuàng)建了緩沖區(qū)來從客戶套接字讀入數(shù)據(jù)奶陈,但是同樣創(chuàng)建了一些字符串存儲這些數(shù)據(jù)。雖然BufferedReader內(nèi)部提供了StringBuffer處理這一問題附较,但是所有的String很快變成了垃圾需要回收吃粒。

同樣的問題在發(fā)送響應(yīng)代碼中也存在

Responseresponse=request.generateResponse();

OutputStreamout=newConnection.getOutputStream();

InputStreamin=response.getInputStream();

intch拒课;

while(-1!=(ch=in.read())){

out.write(ch);

}

newConnection.close();

類似的徐勃,讀寫操作被阻塞而且向流中一次寫入一個字符會造成效率低下事示,所以應(yīng)該使用緩沖區(qū),但是一旦使用緩沖僻肖,流又會產(chǎn)生更多的垃圾肖爵。

傳統(tǒng)的解決方法

通常在Java中處理阻塞I/O要用到線程(大量的線程)。一般是實現(xiàn)一個線程池用來處理請求臀脏,如圖二

圖二

線程使得服務(wù)器可以處理多個連接劝堪,但是它們也同樣引發(fā)了許多問題。每個線程擁有自己的椚嘀桑空間并且占用一些CPU時間秒啦,耗費(fèi)很大,而且很多時間是浪費(fèi)在阻塞的I/O操作上搀玖,沒有有效的利用CPU余境。

三. 新I/O

1.Buffer

傳統(tǒng)的I/O不斷的浪費(fèi)對象資源(通常是String)。新I/O通過使用Buffer讀寫數(shù)據(jù)避免了資源浪費(fèi)灌诅。Buffer對象是線性的芳来,有序的數(shù)據(jù)集合,它根據(jù)其類別只包含唯一的數(shù)據(jù)類型延塑。

java.nio.Buffer類描述

java.nio.ByteBuffer 包含字節(jié)類型绣张。 可以從ReadableByteChannel中讀在 WritableByteChannel中寫

java.nio.MappedByteBuffer 包含字節(jié)類型,直接在內(nèi)存某一區(qū)域映射

java.nio.CharBuffer 包含字符類型关带,不能寫入通道

java.nio.DoubleBuffer 包含double類型侥涵,不能寫入通道

java.nio.FloatBuffer 包含float類型

java.nio.IntBuffer 包含int類型

java.nio.LongBuffer 包含long類型

java.nio.ShortBuffer 包含short類型

可以通過調(diào)用allocate(int capacity)方法或者allocateDirect(int capacity)方法分配一個Buffer。特別的宋雏,你可以創(chuàng)建MappedBytesBuffer通過調(diào)用FileChannel.map(int mode,long position,int size)芜飘。直接(direct)buffer在內(nèi)存中分配一段連續(xù)的塊并使用本地訪問方法讀寫數(shù)據(jù)。非直接(nondirect)buffer通過使用Java中的數(shù)組訪問代碼讀寫數(shù)據(jù)磨总。有時候必須使用非直接緩沖例如使用任何的wrap方法(如ByteBuffer.wrap(byte[]))在Java數(shù)組基礎(chǔ)上創(chuàng)建buffer嗦明。

2. 字符編碼

向ByteBuffer中存放數(shù)據(jù)涉及到兩個問題:字節(jié)的順序和字符轉(zhuǎn)換。ByteBuffer內(nèi)部通過ByteOrder類處理了字節(jié)順序問題蚪燕,但是并沒有處理字符轉(zhuǎn)換娶牌。事實上,ByteBuffer沒有提供方法讀寫String馆纳。

Java.nio.charset.Charset處理了字符轉(zhuǎn)換問題诗良。它通過構(gòu)造CharsetEncoder和CharsetDecoder將字符序列轉(zhuǎn)換成字節(jié)和逆轉(zhuǎn)換。

3. 通道(Channel)

你可能注意到現(xiàn)有的java.io類中沒有一個能夠讀寫B(tài)uffer類型鲁驶,所以NIO中提供了Channel類來讀寫B(tài)uffer鉴裹。通道可以認(rèn)為是一種連接,可以是到特定設(shè)備,程序或者是網(wǎng)絡(luò)的連接径荔。通道的類等級結(jié)構(gòu)圖如下

圖三

圖中ReadableByteChannel和WritableByteChannel分別用于讀寫督禽。

GatheringByteChannel可以從使用一次將多個Buffer中的數(shù)據(jù)寫入通道,相反的总处,ScatteringByteChannel則可以一次將數(shù)據(jù)從通道讀入多個Buffer中狈惫。你還可以設(shè)置通道使其為阻塞或非阻塞I/O操作服務(wù)。

為了使通道能夠同傳統(tǒng)I/O類相容鹦马,Channel類提供了靜態(tài)方法創(chuàng)建Stream或Reader

4.Selector

在過去的阻塞I/O中虱岂,我們一般知道什么時候可以向stream中讀或?qū)懀驗榉椒ㄕ{(diào)用直到stream準(zhǔn)備好時返回菠红。但是使用非阻塞通道第岖,我們需要一些方法來知道什么時候通道準(zhǔn)備好了。在NIO包中试溯,設(shè)計Selector就是為了這個目的蔑滓。SelectableChannel可以注冊特定的事件,而不是在事件發(fā)生時通知應(yīng)用遇绞,通道跟蹤事件键袱。然后,當(dāng)應(yīng)用調(diào)用Selector上的任意一個selection方法時摹闽,它查看注冊了的通道看是否有任何感興趣的事件發(fā)生蹄咖。圖四是selector和兩個已注冊的通道的例子

圖四

并不是所有的通道都支持所有的操作。SelectionKey類定義了所有可能的操作位付鹿,將要用兩次澜汤。首先,當(dāng)應(yīng)用調(diào)用SelectableChannel.register(Selector sel,int op)方法注冊通道時舵匾,它將所需操作作為第二個參數(shù)傳遞到方法中俊抵。然后,一旦SelectionKey被選中了坐梯,SelectionKey的readyOps()方法返回所有通道支持操作的數(shù)位的和徽诲。SelectableChannel的validOps方法返回每個通道允許的操作。注冊通道不支持的操作將引發(fā)IllegalArgumentException異常吵血。下表列出了SelectableChannel子類所支持的操作谎替。

ServerSocketChannelOP_ACCEPT

SocketChannelOP_CONNECT,OP_READ,OP_WRITE

DatagramChannelOP_READ,OP_WRITE

Pipe.SourceChannelOP_READ

Pipe.SinkChannelOP_WRITE

四. 舉例說明

1. 簡單網(wǎng)頁內(nèi)容下載

這個例子非常簡單,類SocketChannelReader使用SocketChannel來下載特定網(wǎng)頁的HTML內(nèi)容蹋辅。

package examples.nio;

importjava.nio.ByteBuffer;

importjava.nio.channels.SocketChannel;

importjava.nio.charset.Charset;

importjava.net.InetSocketAddress;

importjava.io.IOException;

publicclassSocketChannelReader{

privateCharsetcharset=Charset.forName("UTF-8");//創(chuàng)建UTF-8字符集

privateSocketChannelchannel;

publicvoidgetHTMLContent(){

try{

connect();

sendRequest();

readResponse();

}catch(IOExceptione){

System.err.println(e.toString());

}finally{

if(channel!=null){

try{

channel.close();

}catch(IOExceptione){}

}

}

}

privatevoidconnect()throwsIOException{//連接到CSDN

InetSocketAddresssocketAddress=

newInetSocketAddress("http://www.csdn.net",80/);

channel=SocketChannel.open(socketAddress);

//使用工廠方法open創(chuàng)建一個channel并將它連接到指定地址上

//相當(dāng)與SocketChannel.open().connect(socketAddress);調(diào)用

}

privatevoidsendRequest()throwsIOException{

channel.write(charset.encode("GET "

+"/document"

+"\r\n\r\n"));//發(fā)送GET請求到CSDN的文檔中心

//使用channel.write方法钱贯,它需要CharByte類型的參數(shù),使用

//Charset.encode(String)方法轉(zhuǎn)換字符串晕翠。

}

privatevoidreadResponse()throwsIOException{//讀取應(yīng)答

ByteBufferbuffer=ByteBuffer.allocate(1024);//創(chuàng)建1024字節(jié)的緩沖

while(channel.read(buffer)!=-1){

buffer.flip();//flip方法在讀緩沖區(qū)字節(jié)操作之前調(diào)用喷舀。

System.out.println(charset.decode(buffer));

//使用Charset.decode方法將字節(jié)轉(zhuǎn)換為字符串

buffer.clear();//清空緩沖

}

}

publicstaticvoidmain(String[]args){

newSocketChannelReader().getHTMLContent();

}

2. 簡單的加法服務(wù)器和客戶機(jī)

服務(wù)器代碼

packageexamples.nio;

importjava.nio.ByteBuffer;

importjava.nio.IntBuffer;

importjava.nio.channels.ServerSocketChannel;

importjava.nio.channels.SocketChannel;

importjava.net.InetSocketAddress;

importjava.io.IOException;

/**

* SumServer.java

*

*

* Created: Thu Nov 06 11:41:52 2003

*

*@authorstarchu1981

*@version1.0

*/

publicclassSumServer{

privateByteBuffer_buffer=ByteBuffer.allocate(8);

privateIntBuffer_intBuffer=_buffer.asIntBuffer();

privateSocketChannel_clientChannel=null;

privateServerSocketChannel_serverChannel=null;

publicvoidstart(){

try{

openChannel();

waitForConnection();

}catch(IOExceptione){

System.err.println(e.toString());

}

}

privatevoidopenChannel()throwsIOException{

_serverChannel=ServerSocketChannel.open();

_serverChannel.socket().bind(newInetSocketAddress(10000));

System.out.println("服務(wù)器通道已經(jīng)打開");

}

privatevoidwaitForConnection()throwsIOException{

while(true){

_clientChannel=_serverChannel.accept();

if(_clientChannel!=null){

System.out.println("新的連接加入");

processRequest();

_clientChannel.close();

}

}

}

privatevoidprocessRequest()throwsIOException{

_buffer.clear();

_clientChannel.read(_buffer);

intresult=_intBuffer.get(0)+_intBuffer.get(1);

_buffer.flip();

_buffer.clear();

_intBuffer.put(0,result);

_clientChannel.write(_buffer);

}

publicstaticvoidmain(String[]args){

newSumServer().start();

}

}// SumServer

客戶代碼

packageexamples.nio;

importjava.nio.ByteBuffer;

importjava.nio.IntBuffer;

importjava.nio.channels.SocketChannel;

importjava.net.InetSocketAddress;

importjava.io.IOException;

/**

* SumClient.java

*

*

* Created: Thu Nov 06 11:26:06 2003

*

*@authorstarchu1981

*@version1.0

*/

publicclassSumClient{

privateByteBuffer_buffer=ByteBuffer.allocate(8);

privateIntBuffer_intBuffer;

privateSocketChannel_channel;

publicSumClient(){

_intBuffer=_buffer.asIntBuffer();

}// SumClient constructor

publicintgetSum(intfirst,intsecond){

intresult=0;

try{

_channel=connect();

sendSumRequest(first,second);

result=receiveResponse();

}catch(IOExceptione){System.err.println(e.toString());

}finally{

if(_channel!=null){

try{

_channel.close();

}catch(IOExceptione){}

}

}

returnresult;

}

privateSocketChannelconnect()throwsIOException{

InetSocketAddresssocketAddress=

newInetSocketAddress("localhost",10000);

returnSocketChannel.open(socketAddress);

}

privatevoidsendSumRequest(intfirst,intsecond)throwsIOException{

_buffer.clear();

_intBuffer.put(0,first);

_intBuffer.put(1,second);

_channel.write(_buffer);

System.out.println("發(fā)送加法請求?"+first+"+"+second);

}

privateintreceiveResponse()throwsIOException{

_buffer.clear();

_channel.read(_buffer);

return_intBuffer.get(0);

}

publicstaticvoidmain(String[]args){

SumClientsumClient=newSumClient();

System.out.println("加法結(jié)果為?:"+sumClient.getSum(100,324));

}

}// SumClient

3. 非阻塞的加法服務(wù)器

首先在openChannel方法中加入語句

_serverChannel.configureBlocking(false);//設(shè)置成為非阻塞模式

重寫WaitForConnection方法的代碼如下,使用非阻塞方式

privatevoidwaitForConnection()throwsIOException{

SelectoracceptSelector=SelectorProvider.provider().openSelector();

/*在服務(wù)器套接字上注冊selector并設(shè)置為接受accept方法的通知淋肾。

這就告訴Selector硫麻,套接字想要在accept操作發(fā)生時被放在ready表

上,因此樊卓,允許多元非阻塞I/O發(fā)生拿愧。*/

SelectionKeyacceptKey=ssc.register(acceptSelector,

SelectionKey.OP_ACCEPT);

intkeysAdded=0;

/*select方法在任何上面注冊了的操作發(fā)生時返回*/

while((keysAdded=acceptSelector.select())>0){

// 某客戶已經(jīng)準(zhǔn)備好可以進(jìn)行I/O操作了,獲取其ready鍵集合

SetreadyKeys=acceptSelector.selectedKeys();

Iteratori=readyKeys.iterator();

//?遍歷ready鍵集合碌尔,并處理加法請求

while(i.hasNext()){

SelectionKeysk=(SelectionKey)i.next();

i.remove();

ServerSocketChannelnextReady=

(ServerSocketChannel)sk.channel();

//?接受加法請求并處理它

_clientSocket=nextReady.accept().socket();

processRequest();

_clientSocket.close();

}

}

}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末浇辜,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子唾戚,更是在濱河造成了極大的恐慌柳洋,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件叹坦,死亡現(xiàn)場離奇詭異熊镣,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)募书,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進(jìn)店門绪囱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人莹捡,你說我怎么就攤上這事鬼吵。” “怎么了篮赢?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵齿椅,是天一觀的道長。 經(jīng)常有香客問我启泣,道長媒咳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任种远,我火速辦了婚禮涩澡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘坠敷。我一直安慰自己妙同,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布膝迎。 她就那樣靜靜地躺著粥帚,像睡著了一般。 火紅的嫁衣襯著肌膚如雪限次。 梳的紋絲不亂的頭發(fā)上芒涡,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天柴灯,我揣著相機(jī)與錄音,去河邊找鬼费尽。 笑死赠群,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的旱幼。 我是一名探鬼主播查描,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼柏卤!你這毒婦竟也來了冬三?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤缘缚,失蹤者是張志新(化名)和其女友劉穎勾笆,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體桥滨,經(jīng)...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡匠襟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了该园。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片酸舍。...
    茶點(diǎn)故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖里初,靈堂內(nèi)的尸體忽然破棺而出啃勉,到底是詐尸還是另有隱情,我是刑警寧澤双妨,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布淮阐,位于F島的核電站,受9級特大地震影響刁品,放射性物質(zhì)發(fā)生泄漏泣特。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一挑随、第九天 我趴在偏房一處隱蔽的房頂上張望状您。 院中可真熱鬧,春花似錦兜挨、人聲如沸膏孟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽柒桑。三九已至,卻和暖如春噪舀,著一層夾襖步出監(jiān)牢的瞬間魁淳,已是汗流浹背飘诗。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留界逛,地道東北人昆稿。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像仇奶,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子比驻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評論 2 354

推薦閱讀更多精彩內(nèi)容