Java?NIO與IO的區(qū)別以及比較
前言
傳統(tǒng)的socket IO中,需要為每個(gè)連接創(chuàng)建一個(gè)線程,當(dāng)并發(fā)的連接數(shù)量非常巨大時(shí),線程所占用的棧內(nèi)存和CPU線程切換的開銷將非常巨大。使用NIO,不再需要為每個(gè)線程創(chuàng)建單獨(dú)的線程,可以用一個(gè)含有限數(shù)量線程的線程池,甚至一個(gè)線程來為任意數(shù)量的連接服務(wù)。由于線程數(shù)量小于連接數(shù)量,所以每個(gè)線程進(jìn)行IO操作時(shí)就不能阻塞,如果阻塞的話,有些連接就得不到處理,NIO提供了這種非阻塞的能力。
小量的線程如何同時(shí)為大量連接服務(wù)呢,答案就是就緒選擇。這就好比到餐廳吃飯,每來一桌客人,都有一個(gè)服務(wù)員專門為你服務(wù),從你到餐廳到結(jié)帳走人,這樣方式的好處是服務(wù)質(zhì)量好,一對一的服務(wù),VIP啊,可是缺點(diǎn)也很明顯,成本高,如果餐廳生意好,同時(shí)來100桌客人,就需要100個(gè)服務(wù)員,那老板發(fā)工資的時(shí)候得心痛死了,這就是傳統(tǒng)的一個(gè)連接一個(gè)線程的方式。
老板是什么人啊,精著呢。這老板就得捉摸怎么能用10個(gè)服務(wù)員同時(shí)為100桌客人服務(wù)呢,老板就發(fā)現(xiàn),服務(wù)員在為客人服務(wù)的過程中并不是一直都忙著,客人點(diǎn)完菜,上完菜,吃著的這段時(shí)間,服務(wù)員就閑下來了,可是這個(gè)服務(wù)員還是被這桌客人占用著,不能為別的客人服務(wù),用華為領(lǐng)導(dǎo)的話說,就是工作不飽滿。那怎么把這段閑著的時(shí)間利用起來呢。這餐廳老板就想了一個(gè)辦法,讓一個(gè)服務(wù)員(前臺)專門負(fù)責(zé)收集客人的需求,登記下來,比如有客人進(jìn)來了、客人點(diǎn)菜了,客人要結(jié)帳了,都先記錄下來按順序排好。每個(gè)服務(wù)員到這里領(lǐng)一個(gè)需求,比如點(diǎn)菜,就拿著菜單幫客人點(diǎn)菜去了。點(diǎn)好菜以后,服務(wù)員馬上回來,領(lǐng)取下一個(gè)需求,繼續(xù)為別人客人服務(wù)去了。這種方式服務(wù)質(zhì)量就不如一對一的服務(wù)了,當(dāng)客人數(shù)據(jù)很多的時(shí)候可能需要等待。但好處也很明顯,由于在客人正吃飯著的時(shí)候服務(wù)員不用閑著了,服務(wù)員這個(gè)時(shí)間內(nèi)可以為其他客人服務(wù)了,原來10個(gè)服務(wù)員最多同時(shí)為10桌客人服務(wù),現(xiàn)在可能為50桌,60客人服務(wù)了。
這種服務(wù)方式跟傳統(tǒng)的區(qū)別有兩個(gè):
- 1、增加了一個(gè)角色,要有一個(gè)專門負(fù)責(zé)收集客人需求的人。NIO里對應(yīng)的就是Selector。
- 2、由阻塞服務(wù)方式改為非阻塞服務(wù)了,客人吃著的時(shí)候服務(wù)員不用一直侯在客人旁邊了。傳統(tǒng)的IO操作,比如read(),當(dāng)沒有數(shù)據(jù)可讀的時(shí)候,線程一直阻塞被占用,直到數(shù)據(jù)到來。NIO中沒有數(shù)據(jù)可讀時(shí),read()會立即返回0,線程不會阻塞。
NIO中,客戶端創(chuàng)建一個(gè)連接后,先要將連接注冊到Selector,相當(dāng)于客人進(jìn)入餐廳后,告訴前臺你要用餐,前臺會告訴你你的桌號是幾號,然后你就可能到那張桌子坐下了,SelectionKey就是桌號。當(dāng)某一桌需要服務(wù)時(shí),前臺就記錄哪一桌需要什么服務(wù),比如1號桌要點(diǎn)菜,2號桌要結(jié)帳,服務(wù)員從前臺取一條記錄,根據(jù)記錄提供服務(wù),完了再來取下一條。這樣服務(wù)的時(shí)間就被最有效的利用起來了。
導(dǎo)讀:
J2SE1.4以上版本中發(fā)布了全新的I/O類庫。
NIO庫提供的一些新特性:非阻塞I/O,字符轉(zhuǎn)換,緩沖以及通道。NIO和IO都在rt.jar包中。
一、NIO的簡介
NIO包(java.nio.*)引入了四個(gè)關(guān)鍵的抽象數(shù)據(jù)類型,它們共同解決傳統(tǒng)的I/O類中的一些問題。
1. Buffer:它是包含數(shù)據(jù)且用于讀寫的線形表結(jié)構(gòu)。其中還提供了一個(gè)特殊類用于內(nèi)存映射文件的I/O操作。
2. Charset:它提供Unicode字符串影射到字節(jié)序列以及逆影射的操作。
3. Channels:包含socket,file和pipe三種管道,它實(shí)際上是雙向交流的通道。
4. Selector:它將多元異步I/O操作集中到一個(gè)或多個(gè)線程中(它可以被看成是Unix中select()函數(shù)或Win32中WaitForSingleEvent()函數(shù)的面向?qū)ο蟀姹荆?/p>
二、IO的傳統(tǒng)方式
以網(wǎng)絡(luò)應(yīng)用為例,傳統(tǒng)方式需要監(jiān)聽一個(gè)ServerSocket,接受請求的連接為其提供服務(wù)(服務(wù)通常包括了處理請求并發(fā)送響應(yīng))圖一是服務(wù)器的生命周期圖,其中標(biāo)有粗黑線條的部分表明會發(fā)生I/O阻塞。
圖一
可以分析創(chuàng)建服務(wù)器的每個(gè)具體步驟。首先創(chuàng)建ServerSocket
ServerSocket server=new ServerSocket(10000);
然后接受新的連接請求
Socket newCnotallow=server.accept();
對于accept方法的調(diào)用將造成阻塞,直到ServerSocket接受到一個(gè)連接請求為止。一旦連接請求被接受,服務(wù)器可以讀客戶socket中的請求。
InputStream in = newConnection.getInputStream(); InputStreamReader reader = new InputStreamReader(in); BufferedReader buffer = new BufferedReader(reader); Request request = new Request(); while(!request.isComplete()) { String line = buffer.readLine(); request.addLine(line); }
這樣的操作有兩個(gè)問題,首先BufferedReader類的readLine()方法在其緩沖區(qū)未滿時(shí)會造成線程阻塞,只有一定數(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)代碼中也存在:
Response response = request.generateResponse(); OutputStream out = newConnection.getOutputStream(); InputStream in = response.getInputStream(); int ch; while(-1 != (ch = in.read())) { out.write(ch); } newConnection.close();
類似的,讀寫操作被阻塞而且向流中一次寫入一個(gè)字符會造成效率低下,所以應(yīng)該使用緩沖區(qū),但是一旦使用緩沖,流又會產(chǎn)生更多的垃圾。
傳統(tǒng)的解決方法:
通常在Java中處理阻塞I/O要用到線程(大量的線程)。一般是實(shí)現(xiàn)一個(gè)線程池用來處理請求,如圖二
圖二
線程使得服務(wù)器可以處理多個(gè)連接,但是它們也同樣引發(fā)了許多問題。每個(gè)線程擁有自己的??臻g并且占用一些CPU時(shí)間,耗費(fèi)很大,而且很多時(shí)間是浪費(fèi)在阻塞的I/O操作上,沒有有效的利用CPU。
三、NIO的詳細(xì)介紹
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)方法分配一個(gè)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ù)。有時(shí)候必須使用非直接緩沖例如使用任何的wrap方法(如ByteBuffer.wrap(byte[]))在Java數(shù)組基礎(chǔ)上創(chuàng)建buffer。
2. 字符編碼向ByteBuffer中存放數(shù)據(jù)涉及到兩個(gè)問題:字節(jié)的順序和字符轉(zhuǎn)換。ByteBuffer內(nèi)部通過ByteOrder類處理了字節(jié)順序問題,但是并沒有處理字符轉(zhuǎn)換。事實(shí)上,ByteBuffer沒有提供方法讀寫String。
Java.nio.charset.Charset處理了字符轉(zhuǎn)換問題。它通過構(gòu)造CharsetEncoder和CharsetDecoder將字符序列轉(zhuǎn)換成字節(jié)和逆轉(zhuǎn)換。
3. 通道****(Channel)你可能注意到現(xiàn)有的java.io類中沒有一個(gè)能夠讀寫B(tài)uffer類型,所以NIO中提供了Channel類來讀寫B(tài)uffer。通道可以認(rèn)為是一種連接,可以是到特定設(shè)備,程序或者是網(wǎng)絡(luò)的連接。通道的類等級結(jié)構(gòu)圖如下
圖三
圖中ReadableByteChannel和WritableByteChannel分別用于讀寫。
GatheringByteChannel可以從使用一次將多個(gè)Buffer中的數(shù)據(jù)寫入通道,相反的,ScatteringByteChannel則可以一次將數(shù)據(jù)從通道讀入多個(gè)Buffer中。你還可以設(shè)置通道使其為阻塞或非阻塞I/O操作服務(wù)。
為了使通道能夠同傳統(tǒng)I/O類相容,Channel類提供了靜態(tài)方法創(chuàng)建Stream或Reader
4.**** Selector在過去的阻塞I/O中,我們一般知道什么時(shí)候可以向stream中讀或?qū)?,因?yàn)榉椒ㄕ{(diào)用直到stream準(zhǔn)備好時(shí)返回。但是使用非阻塞通道,我們需要一些方法來知道什么時(shí)候通道準(zhǔn)備好了。在NIO包中,設(shè)計(jì)Selector就是為了這個(gè)目的。SelectableChannel可以注冊特定的事件,而不是在事件發(fā)生時(shí)通知應(yīng)用,通道跟蹤事件。然后,當(dāng)應(yīng)用調(diào)用Selector上的任意一個(gè)selection方法時(shí),它查看注冊了的通道看是否有任何感興趣的事件發(fā)生。
圖四是selector和兩個(gè)已注冊的通道的例子:
圖四
并不是所有的通道都支持所有的操作。SelectionKey類定義了所有可能的操作位,將要用兩次。首先,當(dāng)應(yīng)用調(diào)用SelectableChannel.register(Selector sel,int op)方法注冊通道時(shí),它將所需操作作為第二個(gè)參數(shù)傳遞到方法中。然后,一旦SelectionKey被選中了,SelectionKey的readyOps()方法返回所有通道支持操作的數(shù)位的和。SelectableChannel的validOps方法返回每個(gè)通道允許的操作。注冊通道不支持的操作將引發(fā)IllegalArgumentException異常。下表列出了SelectableChannel子類所支持的操作。
ServerSocketChannel OP_ACCEPT SocketChannel OP_CONNECT, OP_READ, OP_WRITE DatagramChannel OP_READ, OP_WRITE Pipe.SourceChannel OP_READ Pipe.SinkChannel OP_WRITE
四. 舉例說明
1. 簡單網(wǎng)頁內(nèi)容下載這個(gè)例子非常簡單,類SocketChannelReader使用SocketChannel來下載特定網(wǎng)頁的HTML內(nèi)容。
package examples.nio;
package com.yineng.mycat; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; import java.net.InetSocketAddress; import java.io.IOException; /** * @author kzfy * @data 2015/12/21 */ public class SocketChannelReader{ private Charset charset=Charset.forName("UTF-8");//創(chuàng)建UTF-8字符集 private SocketChannel channel; public void getHTMLContent(){ try{ connect(); sendRequest(); readResponse(); }catch(IOException e){ System.err.println(e.toString()); }finally{ if(channel!=null){ try{ channel.close(); }catch(IOException e){} } } } private void connect()throws IOException{//連接到CSDN InetSocketAddress socketAddress= new InetSocketAddress("http://www.csdn.net",80); channel=SocketChannel.open(socketAddress); //使用工廠方法open創(chuàng)建一個(gè)channel并將它連接到指定地址上 //相當(dāng)與SocketChannel.open().connect(socketAddress);調(diào)用 } private void sendRequest()throws IOException{ channel.write(charset.encode("GET /document\r\n\r\n"));//發(fā)送GET請求到CSDN的文檔中心 //使用channel.write方法,它需要CharByte類型的參數(shù),使用 //Charset.encode(String)方法轉(zhuǎn)換字符串。 } private void readResponse()throws IOException{//讀取應(yīng)答 ByteBuffer buffer=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();//清空緩沖 } } public static void main(String [] args){ new SocketChannelReader().getHTMLContent(); } }
到此這篇關(guān)于Java NIO與IO的區(qū)別以及比較的文章就介紹到這了,更多相關(guān)Java NIO與IO 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springMVC+jersey實(shí)現(xiàn)跨服務(wù)器文件上傳
這篇文章主要介紹了springMVC+jersey實(shí)現(xiàn)跨服務(wù)器文件上傳,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08SpringBoot實(shí)現(xiàn)excel生成并且通過郵件發(fā)送的步驟詳解
實(shí)際開發(fā)中,特別是在B端產(chǎn)品的開發(fā)中,我們經(jīng)常會遇到導(dǎo)出excel的功能,更進(jìn)階一點(diǎn)的需要我們定期生成統(tǒng)計(jì)報(bào)表,然后通過郵箱發(fā)送給指定的人員,?今天要帶大家來實(shí)現(xiàn)的就是excel生成并通過郵件發(fā)送,需要的朋友可以參考下2023-10-10關(guān)于Integer.parseInt()方法的使用
這篇文章主要介紹了關(guān)于Integer.parseInt()方法的使用,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-11-11Springboot實(shí)現(xiàn)緩存預(yù)熱的方法
在系統(tǒng)啟動之前通過預(yù)先將常用數(shù)據(jù)加載到緩存中,以提高緩存命中率和系統(tǒng)性能的過程,緩存預(yù)熱的目的是盡可能地避免緩存擊穿和緩存雪崩,這篇文章主要介紹了Springboot實(shí)現(xiàn)緩存預(yù)熱,需要的朋友可以參考下2024-03-03GsonFormat快速生成JSon實(shí)體類的實(shí)現(xiàn)
GsonFormat主要用于使用Gson庫將JSONObject格式的String?解析成實(shí)體,本文主要介紹了GsonFormat快速生成JSon實(shí)體類的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2023-05-05VsCode搭建Spring Boot項(xiàng)目并進(jìn)行創(chuàng)建、運(yùn)行、調(diào)試
這篇文章主要介紹了VsCode搭建Spring Boot項(xiàng)目并進(jìn)行創(chuàng)建、運(yùn)行、調(diào)試 ,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05MyBatis中example.createCriteria()方法的具體使用
本文詳細(xì)介紹了MyBatis的Example工具的使用方法,包括鏈?zhǔn)秸{(diào)用指定字段、設(shè)置查詢條件、支持多種查詢方式等,還介紹了mapper的crud方法、and/or方法的使用,以及如何進(jìn)行多條件和多重條件查詢,感興趣的可以了解一下2024-10-10