前言:在之前的面試中绳矩,每每問(wèn)到關(guān)于Java I/O 方面的東西都感覺(jué)自己吃了大虧..所以這里搶救一下..來(lái)深入的了解一下在Java之中的 I/O 到底是怎么回事..文章可能說(shuō)明類的文字有點(diǎn)兒多,希望能耐心讀完..
什么是 I/O?
學(xué)習(xí)過(guò)計(jì)算機(jī)相關(guān)課程的童鞋應(yīng)該都知道玖翅,I/O 即輸入Input/ 輸出Output的縮寫翼馆,最容易讓人聯(lián)想到的就是屏幕這樣的輸出設(shè)備以及鍵盤鼠標(biāo)這一類的輸入設(shè)備,其廣義上的定義就是:數(shù)據(jù)在內(nèi)部存儲(chǔ)器和外部存儲(chǔ)器或其他周邊設(shè)備之間的輸入和輸出金度;
我們可以從定義上看到問(wèn)題的核心就是:數(shù)據(jù)/ 輸入/ 輸出应媚,在Java中,主要就是涉及到磁盤 I/O 和網(wǎng)絡(luò) I/O 兩種了猜极;
簡(jiǎn)單理解Java 流(Stream)
通常我們說(shuō) I/O 都會(huì)涉及到諸如輸入流中姜、輸出流這樣的概念,那么什么是流呢跟伏?流是一個(gè)抽象但形象的概念丢胚,你可以簡(jiǎn)單理解成一個(gè)數(shù)據(jù)的序列翩瓜,輸入流表示從一個(gè)源讀取數(shù)據(jù),輸出流則表示向一個(gè)目標(biāo)寫數(shù)據(jù)嗜桌,在Java程序中奥溺,對(duì)于數(shù)據(jù)的輸入和輸出都是采用 “流” 這樣的方式進(jìn)行的,其設(shè)備可以是文件骨宠、網(wǎng)絡(luò)浮定、內(nèi)存等;
流具有方向性层亿,至于是輸入流還是輸出流則是一個(gè)相對(duì)的概念桦卒,一般以程序?yàn)閰⒖迹绻麛?shù)據(jù)的流向是程序至設(shè)備匿又,我們成為輸出流方灾,反之我們稱為輸入流。
可以將流想象成一個(gè)“水流管道”碌更,水流就在這管道中形成了裕偿,自然就出現(xiàn)了方向的概念。
“流”痛单,代表了任何有能力產(chǎn)出數(shù)據(jù)的數(shù)據(jù)源對(duì)象或有能力接受數(shù)據(jù)的接收端對(duì)象嘿棘,它屏蔽了實(shí)際的 I/O 設(shè)備中處理數(shù)據(jù)的細(xì)節(jié)——摘自《Think in Java》
參考資料:深入理解 Java中的 流 (Stream):https://www.cnblogs.com/shitouer/archive/2012/12/19/2823641.html
Java中的 I/O 類庫(kù)的基本架構(gòu)
I/O 問(wèn)題是任何編程語(yǔ)言都無(wú)法回避的問(wèn)題,因?yàn)?I/O 操作是人機(jī)交互的核心旭绒,是機(jī)器獲取和交換信息的主要渠道鸟妙,所以如何設(shè)計(jì) I/O 系統(tǒng)變成了一大難題,特別是在當(dāng)今大流量大數(shù)據(jù)的時(shí)代挥吵,I/O 問(wèn)題尤其突出重父,很容易稱為一個(gè)性能的瓶頸,也正因?yàn)槿绱撕鲂伲?I/O 庫(kù)上也一直在做持續(xù)的優(yōu)化房午,例如JDK1.4引入的 NIO,JDK1.7引入的 NIO 2.0丹允,都一定程度上的提升了 I/O 的性能歪沃;
Java的 I/O 操作類在包 java.io下,有將近80個(gè)類嫌松,這些類大概可以分成如下 4 組:
- 基于字節(jié)操作的 I/O 接口:InputStream 和 OutputStream;
- 基于字符操作的 I/O 接口:Writer 和 Reader奕污;
- 基于磁盤操作的 I/O 接口:File萎羔;
- 基于網(wǎng)絡(luò)操作的 I/O 接口:Socket;
前兩組主要是傳輸數(shù)據(jù)的數(shù)據(jù)格式碳默,后兩組主要是傳輸數(shù)據(jù)的方式贾陷,雖然Socket類并不在java.io包下缘眶,但這里仍然把它們劃分在了一起;I/O 只是人機(jī)交互的一種手段髓废,除了它們能夠完成這個(gè)交互功能外巷懈,我們更多的應(yīng)該是關(guān)注如何提高它的運(yùn)行效率;
00.基于字節(jié)的 I/O 操作接口
基于字節(jié)的 I/O 操作的接口輸入和輸出分別對(duì)應(yīng)是 InputStream 和 OutputStream慌洪,InputStream 的類層次結(jié)構(gòu)如下圖:
輸入流根據(jù)數(shù)據(jù)類型和操作方式又被劃分成若干個(gè)子類顶燕,每個(gè)子類分別處理不同操作類型,OutputStream 輸出流的類層次結(jié)構(gòu)也是類似冈爹,如下圖所示:
這里就不詳細(xì)解釋每個(gè)子類如何使用了涌攻,如果感興趣可以自己去看一下JDK的源碼,而且的話從類名也能大致看出一二該類是在處理怎樣的一些東西..這里需要說(shuō)明兩點(diǎn):
1)操作數(shù)據(jù)的方式是可以組合使用的:
例如:
OutputStream out = new BufferedOutputStream(new ObjectOutputStream(new FileOutputStream("fileName"))频伤;
2)必須要指定流最終寫到什么地方:
要么是寫到磁盤恳谎,要么是寫到網(wǎng)絡(luò)中,但重點(diǎn)是你必須說(shuō)明這一點(diǎn)憋肖,而且你會(huì)發(fā)現(xiàn)其實(shí)SocketOutputStream是屬于FileOutputStream下的因痛,也就是說(shuō)寫網(wǎng)絡(luò)實(shí)際上也是寫文件,只不過(guò)寫網(wǎng)絡(luò)還有一步需要處理岸更,就是讓底層的操作系統(tǒng)知道我這個(gè)數(shù)據(jù)是需要傳送到其他地方而不是本地磁盤上的鸵膏;
01.基于字符的 I/O 操作接口
不管是磁盤還是網(wǎng)絡(luò)傳輸,最小的存儲(chǔ)單元都是字節(jié)坐慰,而不是字符较性,所以 I/O 操作的都是字節(jié)而不是字符,但是在我們?nèi)粘5某绦蛑胁僮鞯臄?shù)據(jù)幾乎都是字符结胀,所以為了操作方便當(dāng)然要提供一個(gè)可以直接寫字符的 I/O 接口赞咙。而且從字符到字節(jié)必須經(jīng)過(guò)編碼轉(zhuǎn)換,而這個(gè)編碼又非常耗時(shí)糟港,還經(jīng)常出現(xiàn)亂碼的問(wèn)題攀操,所以 I/O 的編碼問(wèn)題經(jīng)常是讓人頭疼的問(wèn)題,關(guān)于這個(gè)問(wèn)題有一篇深度好文推薦一下:《深入分析 Java 中的中文編碼問(wèn)題》
下圖是寫字符的 I/O 操作接口涉及到的類秸抚,Writer 類提供了一個(gè)抽象方法 write(char cbuf[], int off, int len) 由子類去實(shí)現(xiàn):
讀字符的操作接口也有類似的類結(jié)構(gòu)速和,如下圖所示:
讀字符的操作接口中也是 int read(char cbuf[], int off, int len),返回讀到的 n 個(gè)字節(jié)數(shù)剥汤,不管是 Writer 還是 Reader 類它們都只定義了讀取或?qū)懭氲臄?shù)據(jù)字符的方式颠放,也就是怎么寫或讀,但是并沒(méi)有規(guī)定數(shù)據(jù)要寫到哪去吭敢,寫到哪去就是我們后面要討論的基于磁盤和網(wǎng)絡(luò)的工作機(jī)制碰凶。
01.字節(jié)與字符的轉(zhuǎn)化接口
另外數(shù)據(jù)持久化或網(wǎng)絡(luò)傳輸都是以字節(jié)進(jìn)行的,所以必須要有字符到字節(jié)或字節(jié)到字符的轉(zhuǎn)化。字符到字節(jié)需要轉(zhuǎn)化欲低,其中讀的轉(zhuǎn)化過(guò)程如下圖所示:
InputStreamReader 類是字節(jié)到字符的轉(zhuǎn)化橋梁辕宏,InputStream 到 Reader 的過(guò)程要指定編碼字符集,否則將采用操作系統(tǒng)默認(rèn)字符集砾莱,很可能會(huì)出現(xiàn)亂碼問(wèn)題瑞筐。StreamDecoder 正是完成字節(jié)到字符的解碼的實(shí)現(xiàn)類。也就是當(dāng)你用如下方式讀取一個(gè)文件時(shí):
try {
StringBuffer str = new StringBuffer();
char[] buf = new char[1024];
FileReader f = new FileReader("file");
while(f.read(buf)>0){
str.append(buf);
}
str.toString();
} catch (IOException e) {}
FileReader 類就是按照上面的工作方式讀取文件的腊瑟,F(xiàn)ileReader 是繼承了 InputStreamReader 類聚假,實(shí)際上是讀取文件流,然后通過(guò) StreamDecoder 解碼成 char扫步,只不過(guò)這里的解碼字符集是默認(rèn)字符集魔策。
寫入也是類似的過(guò)程如下圖所示:
通過(guò) OutputStreamWriter 類完成,字符到字節(jié)的編碼過(guò)程河胎,由 StreamEncoder 完成編碼過(guò)程闯袒。
磁盤 I/O 的工作機(jī)制
在介紹 Java 讀取和寫入磁盤文件之前,先來(lái)看看應(yīng)用程序訪問(wèn)文件有哪幾種方式游岳;
幾種訪問(wèn)文件的方式
我們知道政敢,讀取和寫入文件 I/O 操作都調(diào)用的是操作系統(tǒng)提供給我們的接口,因?yàn)榇疟P設(shè)備是歸操作系統(tǒng)管的胚迫,而只要是系統(tǒng)調(diào)用都可能存在內(nèi)核空間地址和用戶空間地址切換的問(wèn)題喷户,這是為了保證用戶進(jìn)程不能直接操作內(nèi)核,保證內(nèi)核的安全而設(shè)計(jì)的访锻,現(xiàn)代的操作系統(tǒng)將虛擬空間劃分成了內(nèi)核空間和用戶空間兩部分并實(shí)現(xiàn)了隔離褪尝,但是這樣雖然保證了內(nèi)核程序運(yùn)行的安全性,但是也必然存在數(shù)據(jù)可能需要從內(nèi)核空間向用戶用戶空間復(fù)制的問(wèn)題期犬;
如果遇到非常耗時(shí)的操作河哑,如磁盤 I/O,數(shù)據(jù)從磁盤復(fù)制到內(nèi)核空間龟虎,然后又從內(nèi)核空間復(fù)制到用戶空間璃谨,將會(huì)非常耗時(shí),這時(shí)操作系統(tǒng)為了加速 I/O 訪問(wèn)鲤妥,在內(nèi)核空間使用緩存機(jī)制佳吞,也就是將從磁盤讀取的文件按照一定的組織方式進(jìn)行緩存,入股用戶程序訪問(wèn)的是同一段磁盤地址的空間數(shù)據(jù)棉安,那么操作系統(tǒng)將從內(nèi)核緩存中直接取出返回給用戶程序底扳,這樣就可以減少 I/O 的響應(yīng)時(shí)間;
00. 標(biāo)準(zhǔn)訪問(wèn)文件的方式
讀取的方式是贡耽,當(dāng)應(yīng)用程序調(diào)用read()
接口時(shí):
- ①操作系統(tǒng)首先檢查在內(nèi)核的高速緩存中是否存在需要的數(shù)據(jù)花盐,如果有羡滑,那么直接從緩存中返回;
- ②如果沒(méi)有算芯,則從磁盤中讀取,然后緩存在操作系統(tǒng)的緩存中凳宙;
寫入的方式是熙揍,當(dāng)應(yīng)用程序調(diào)用write()
接口時(shí):
- 從用戶地址空間復(fù)制到內(nèi)核地址空間的緩存中,這時(shí)對(duì)用戶程序來(lái)說(shuō)寫操作就已經(jīng)完成了氏涩,至于什么時(shí)候在寫到磁盤中由操作系統(tǒng)決定届囚,除非顯示地調(diào)用了 sync 同步命令;
01.直接 I/O 方式
所謂的直接 I/O 的方式就是應(yīng)用程序直接訪問(wèn)磁盤數(shù)據(jù)是尖,而不經(jīng)過(guò)操作系統(tǒng)內(nèi)核數(shù)據(jù)緩沖區(qū)意系,這樣做的目的是減少一次從內(nèi)核緩沖區(qū)到用戶程序緩存的數(shù)據(jù)復(fù)制;
這種訪問(wèn)文件的方式通常是在對(duì)數(shù)據(jù)的緩存管理由應(yīng)用程序?qū)崿F(xiàn)的數(shù)據(jù)庫(kù)管理系統(tǒng)中饺汹,如在數(shù)據(jù)庫(kù)管理系統(tǒng)中蛔添,系統(tǒng)明確地知道應(yīng)該緩存哪些數(shù)據(jù),應(yīng)該失效哪些數(shù)據(jù)兜辞,還可以對(duì)一些熱點(diǎn)數(shù)據(jù)做預(yù)加載迎瞧,提前將熱點(diǎn)數(shù)據(jù)加載到內(nèi)存,可以加速數(shù)據(jù)的訪問(wèn)效率逸吵,而這些情況如果是交給操作系統(tǒng)進(jìn)行緩存凶硅,那么操作系統(tǒng)將不知道哪些數(shù)據(jù)是熱點(diǎn)數(shù)據(jù),哪些是只會(huì)訪問(wèn)一次的數(shù)據(jù)扫皱,因?yàn)樗皇呛?jiǎn)單的緩存最近一次從磁盤讀取的數(shù)據(jù)而已足绅;
但是直接 I/O 也有負(fù)面影響,如果訪問(wèn)的數(shù)據(jù)不再應(yīng)用程序緩存之中韩脑,那么每次數(shù)據(jù)都會(huì)直接從磁盤進(jìn)行加載氢妈,這種直接加載會(huì)非常緩慢,因此直接 I/O 通常與 異步 I/O 進(jìn)行結(jié)合以達(dá)到更好的性能扰才;
10.內(nèi)存映射的方式
內(nèi)存映射是指將硬盤上文件的位置與進(jìn)程邏輯地址空間中一塊大小相同的區(qū)域一一對(duì)應(yīng)允懂,當(dāng)要訪問(wèn)內(nèi)存中一段數(shù)據(jù)時(shí),轉(zhuǎn)換為訪問(wèn)文件的某一段數(shù)據(jù)衩匣。這種方式的目的同樣是減少數(shù)據(jù)在用戶空間和內(nèi)核空間之間的拷貝操作蕾总。當(dāng)大量數(shù)據(jù)需要傳輸?shù)臅r(shí)候,采用內(nèi)存映射方式去訪問(wèn)文件會(huì)獲得比較好的效率琅捏。
同步和異步訪問(wèn)文件的方式
另外還有兩種方式生百,一種是數(shù)據(jù)的讀取和寫入都是同步操作的同步方式,另一種是是當(dāng)訪問(wèn)數(shù)據(jù)的線程發(fā)出請(qǐng)求之后柄延,線程會(huì)接著去處理其他事情蚀浆,而不是阻塞等待的異步訪問(wèn)方式缀程,但從筆者就《深入分析 Java Web技術(shù)內(nèi)幕》一書中的內(nèi)容來(lái)看,這兩種方式更像是對(duì)標(biāo)準(zhǔn)訪問(wèn)方式的一個(gè)具體說(shuō)明市俊,是標(biāo)準(zhǔn)訪問(wèn)方式對(duì)應(yīng)的兩種不同處理方法杨凑,知道就好了...
Java 訪問(wèn)磁盤文件
我們知道數(shù)據(jù)在磁盤的唯一最小描述就是文件,也就是說(shuō)上層應(yīng)用程序只能通過(guò)文件來(lái)操作磁盤上的數(shù)據(jù)摆昧,文件也是操作系統(tǒng)和磁盤驅(qū)動(dòng)器交互的一個(gè)最小單元撩满。值得注意的是 Java 中通常的 File 并不代表一個(gè)真實(shí)存在的文件對(duì)象,當(dāng)你通過(guò)指定一個(gè)路徑描述符時(shí)绅你,它就會(huì)返回一個(gè)代表這個(gè)路徑相關(guān)聯(lián)的一個(gè)虛擬對(duì)象伺帘,這個(gè)可能是一個(gè)真實(shí)存在的文件或者是一個(gè)包含多個(gè)文件的目錄。為何要這樣設(shè)計(jì)忌锯?因?yàn)榇蟛糠智闆r下伪嫁,我們并不關(guān)心這個(gè)文件是否真的存在,而是關(guān)心這個(gè)文件到底如何操作偶垮。例如我們手機(jī)里通常存了幾百個(gè)朋友的電話號(hào)碼张咳,但是我們通常關(guān)心的是我有沒(méi)有這個(gè)朋友的電話號(hào)碼,或者這個(gè)電話號(hào)碼是什么针史,但是這個(gè)電話號(hào)碼到底能不能打通晶伦,我們并不是時(shí)時(shí)刻刻都去檢查,而只有在真正要給他打電話時(shí)才會(huì)看這個(gè)電話能不能用啄枕。也就是使用這個(gè)電話記錄要比打這個(gè)電話的次數(shù)多很多婚陪。
何時(shí)真正會(huì)要檢查一個(gè)文件存不存?就是在真正要讀取這個(gè)文件時(shí)频祝,例如 FileInputStream 類都是操作一個(gè)文件的接口泌参,注意到在創(chuàng)建一個(gè) FileInputStream 對(duì)象時(shí),會(huì)創(chuàng)建一個(gè) FileDescriptor 對(duì)象常空,其實(shí)這個(gè)對(duì)象就是真正代表一個(gè)存在的文件對(duì)象的描述沽一,當(dāng)我們?cè)诓僮饕粋€(gè)文件對(duì)象時(shí)可以通過(guò) getFD() 方法獲取真正操作的與底層操作系統(tǒng)關(guān)聯(lián)的文件描述。例如可以調(diào)用 FileDescriptor.sync() 方法將操作系統(tǒng)緩存中的數(shù)據(jù)強(qiáng)制刷新到物理磁盤中漓糙。
下面以上文讀取文件的程序?yàn)槔巢榻B下如何從磁盤讀取一段文本字符。如下圖所示:
當(dāng)傳入一個(gè)文件路徑昆禽,將會(huì)根據(jù)這個(gè)路徑創(chuàng)建一個(gè) File 對(duì)象來(lái)標(biāo)識(shí)這個(gè)文件蝗蛙,然后將會(huì)根據(jù)這個(gè) File 對(duì)象創(chuàng)建真正讀取文件的操作對(duì)象,這時(shí)將會(huì)真正創(chuàng)建一個(gè)關(guān)聯(lián)真實(shí)存在的磁盤文件的文件描述符 FileDescriptor醉鳖,通過(guò)這個(gè)對(duì)象可以直接控制這個(gè)磁盤文件捡硅。由于我們需要讀取的是字符格式,所以需要 StreamDecoder 類將 byte 解碼為 char 格式盗棵,至于如何從磁盤驅(qū)動(dòng)器上讀取一段數(shù)據(jù)壮韭,由操作系統(tǒng)幫我們完成北发。至于操作系統(tǒng)是如何將數(shù)據(jù)持久化到磁盤以及如何建立數(shù)據(jù)結(jié)構(gòu)需要根據(jù)當(dāng)前操作系統(tǒng)使用何種文件系統(tǒng)來(lái)回答,至于文件系統(tǒng)的相關(guān)細(xì)節(jié)可以參考另外的文章喷屋。
參考文章:深入分析 Java I/O 的工作機(jī)制
關(guān)于這一part琳拨,我們只需要了解一下就可以,我也是直接復(fù)制就完事兒...
Java 序列化技術(shù)
Java序列化就是將一個(gè)對(duì)象轉(zhuǎn)化成一串二進(jìn)制表示的字節(jié)數(shù)組屯曹,通過(guò)保存或轉(zhuǎn)移這些字節(jié)數(shù)據(jù)來(lái)達(dá)到持久化的目的从绘。需要持久化,對(duì)象必須繼承 java.io.Serializable
接口是牢,或者將其轉(zhuǎn)為字節(jié)數(shù)組,用于網(wǎng)絡(luò)傳輸陕截;
一個(gè)實(shí)際的序列化例子
第一步:創(chuàng)建一個(gè)用于序列化的對(duì)象
為了具體說(shuō)明序列化在Java中是如何運(yùn)作的驳棱,我們來(lái)寫一個(gè)實(shí)際的例子,首先我們來(lái)寫一個(gè)用于序列化的對(duì)象农曲,然后實(shí)現(xiàn)上述的接口:
/**
* 用于演示Java中序列化的工作流程...
*
* @author: @我沒(méi)有三顆心臟
* @create: 2018-08-15-下午 14:37
*/
public class People implements Serializable{
public String name;
public transient int age;
public void sayHello() {
System.out.println("Hello,My Name is " + name);
}
}
注意:一個(gè)類的對(duì)象想要序列化成功社搅,必須滿足兩個(gè)條件
- ①實(shí)現(xiàn)上述的接口;
- ②保證該類的所有屬性必須都是可序列化的乳规,如果不希望某個(gè)屬性序列化(例如一些敏感信息)形葬,可以加上
transient
關(guān)鍵字;
第二步:序列化對(duì)象
如下的代碼完成了實(shí)例化一個(gè) People 對(duì)象并其序列化到D盤的根目錄下的一個(gè)操作暮的,這里呢按照 Java 的標(biāo)準(zhǔn)約定將文件的后綴寫成 .ser 的樣子笙以,你也可以寫成其他的...
People people = new People();
people.name = "我沒(méi)有三顆心臟";
people.age = 21;
try {
FileOutputStream fileOutputStream = new FileOutputStream("D:/people.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOutputStream);
out.writeObject(people);
out.close();
fileOutputStream.close();
System.out.println("Serialized data is saved in D:/");
} catch (IOException e) {
e.printStackTrace();
}
第三步:反序列化對(duì)象
下面的程序完成了對(duì)剛才我們序列化的文件還原成一個(gè)People對(duì)象的過(guò)程,并獲取了其中的參數(shù)冻辩,但是注意猖腕,由于我們希望 age 屬性是短暫的加入了transient
關(guān)鍵字, 所以我們無(wú)法獲取到序列化時(shí) People 的 age 屬性:
People people = null;
try {
FileInputStream fileIn = new FileInputStream("D:/people.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
people = (People) in.readObject();
in.close();
fileIn.close();
} catch (IOException i) {
i.printStackTrace();
return;
} catch (ClassNotFoundException c) {
System.out.println("People class not found");
c.printStackTrace();
return;
}
System.out.println("Deserialized People...");
System.out.println("Name: " + people.name);
System.out.println("Age: " + people.age);
輸出結(jié)果如下:
Deserialized People...
Name: 我沒(méi)有三顆心臟
Age: 0
serialVersionUID的作用
上述的例子中我們完成了對(duì)一個(gè) People 對(duì)象序列化和反序列化的過(guò)程恨闪,我們現(xiàn)在來(lái)做一點(diǎn)簡(jiǎn)單的修改倘感,例如把a(bǔ)ge字段的transient
關(guān)鍵字去掉:
public class People implements Serializable {
public String name;
public int age;
public void sayHello() {
System.out.println("Hello,My Name is " + name);
}
}
然后我們?cè)龠\(yùn)行我們剛才反序列化的代碼,會(huì)發(fā)現(xiàn)咙咽,這個(gè)時(shí)候程序竟然報(bào)錯(cuò)了老玛,說(shuō)是serialVersionUID不一致:
事實(shí)上,如果你經(jīng)尘ǎ看別人的代碼的話蜡豹,或許會(huì)有留意到諸如這樣的代碼:
private static final long serialVersionUID = 876323262645176354L;
就這一長(zhǎng)串的東西也不知道是在干嘛的,但這其實(shí)是為了保證序列化版本的兼容性犁享,即在版本升級(jí)后序列化仍保持對(duì)象的唯一性余素;我們通過(guò)上述的修改也感受到了其中的一二,但是問(wèn)題是:我們并沒(méi)有在需要序列化的對(duì)象中寫任何關(guān)于這個(gè)UID的代碼呀炊昆?
這是個(gè)有趣的問(wèn)題桨吊,通常情況下威根,如果我們實(shí)現(xiàn)了序列化接口,但是沒(méi)有自己顯式的聲明這個(gè)UID的話视乐,那么JVM就會(huì)根據(jù)該類的類名洛搀、屬性名、方法名等自己計(jì)算出一個(gè)獨(dú)一無(wú)二的變量值佑淀,然后將這個(gè)變量值一同序列化到文件之中留美,而在反序列化的時(shí)候同樣终娃,會(huì)根據(jù)該類計(jì)算出一個(gè)獨(dú)一無(wú)二的變量然后進(jìn)行比較幼驶,不一致就會(huì)報(bào)錯(cuò)报咳,但是我懷著強(qiáng)烈的好奇心去反編譯了一下.class文件籽暇,并沒(méi)有發(fā)現(xiàn)編譯器寫了UDI這一類的東西郊楣,我看《深入分析 Java Web 技術(shù)內(nèi)幕》中說(shuō)套啤,實(shí)際上是寫到了二進(jìn)制文件里面了固蛾;
- 不顯式聲明的缺點(diǎn):一旦寫好了某一個(gè)類搪缨,那么想要修改就不行了碉哑,所以我們最好自己顯式的去聲明挚币;
- 顯式聲明的方式:①使用默認(rèn)的1L作用UID;②根據(jù)類名扣典、接口名等生成一個(gè)64位的哈希字段妆毕,現(xiàn)在的編譯器如IDEA、Eclipse都有這樣的功能贮尖,大家感興趣去了解下笛粘;
序列化用來(lái)干什么?
雖然我們上面的程序成功將一個(gè)對(duì)象序列化保存到磁盤远舅,然后從磁盤還原闰蛔,但是這樣的功能到底可以應(yīng)用在哪些場(chǎng)景?到底可以干一些什么樣的事情呢图柏?下面舉一些在實(shí)際應(yīng)用中的例子:
- Web服務(wù)器中保存Session對(duì)象序六,如Tomcat會(huì)在服務(wù)器關(guān)閉時(shí)把session序列化存儲(chǔ)到一個(gè)名為session.ser的文件之中,這個(gè)過(guò)程稱為session的鈍化蚤吹;
- 網(wǎng)絡(luò)上傳輸對(duì)象例诀,如分布式應(yīng)用等;
關(guān)于序列化的一些細(xì)節(jié)
1.如果一個(gè)類沒(méi)有實(shí)現(xiàn)Serializable接口裁着,但是它的基類實(shí)現(xiàn)了繁涂,那么這個(gè)類也是可以序列化的;
2.相反二驰,如果一個(gè)類實(shí)現(xiàn)了Serializable接口扔罪,但是它的父類沒(méi)有實(shí)現(xiàn),那么這個(gè)類還是可以序列化(Object是所有類的父類)桶雀,但是序列化該子類對(duì)象矿酵,然后反序列化后輸出父類定義的某變量的數(shù)值唬复,會(huì)發(fā)現(xiàn)該變量數(shù)值與序列化時(shí)的數(shù)值不同(一般為null或者其他默認(rèn)值),而且這個(gè)父類里面必須有無(wú)參的構(gòu)造方法全肮,不然子類反序列化的時(shí)候會(huì)報(bào)錯(cuò)敞咧。
了解到這里就可以了,更多的細(xì)節(jié)感興趣的童鞋可以自行去搜索引擎搜索..
網(wǎng)絡(luò) I/O 工作機(jī)制
數(shù)據(jù)從一臺(tái)主機(jī)發(fā)送到網(wǎng)絡(luò)中的另一臺(tái)主機(jī)需要經(jīng)過(guò)很多步驟辜腺,首先雙方需要有溝通的意向休建,然后要有能夠溝通的物理渠道(物理鏈路),其次评疗,還要保障雙方能夠正常的進(jìn)行交流测砂,例如語(yǔ)言一致的問(wèn)題、說(shuō)話順序的問(wèn)題等等等百匆;
Java Socket 的工作機(jī)制
看到有地方說(shuō):網(wǎng)絡(luò) I/O 的實(shí)質(zhì)其實(shí)就是對(duì) Socket 的讀纫乇搿;那Socket 這個(gè)概念沒(méi)有對(duì)應(yīng)到一個(gè)具體的實(shí)體胧华,它是描述計(jì)算機(jī)之間完成相互通信一種抽象功能。打個(gè)比方宙彪,可以把 Socket 比作為兩個(gè)城市之間的交通工具矩动,有了它,就可以在城市之間來(lái)回穿梭了释漆。交通工具有多種悲没,每種交通工具也有相應(yīng)的交通規(guī)則。Socket 也一樣男图,也有多種示姿。大部分情況下我們使用的都是基于 TCP/IP 的流套接字,它是一種穩(wěn)定的通信協(xié)議逊笆。
下圖是典型的基于 Socket 的通信的場(chǎng)景:
主機(jī) A 的應(yīng)用程序要能和主機(jī) B 的應(yīng)用程序通信栈戳,必須通過(guò) Socket 建立連接,而建立 Socket 連接必須需要底層 TCP/IP 協(xié)議來(lái)建立 TCP 連接难裆。建立 TCP 連接需要底層 IP 協(xié)議來(lái)尋址網(wǎng)絡(luò)中的主機(jī)子檀。我們知道網(wǎng)絡(luò)層使用的 IP 協(xié)議可以幫助我們根據(jù) IP 地址來(lái)找到目標(biāo)主機(jī),但是一臺(tái)主機(jī)上可能運(yùn)行著多個(gè)應(yīng)用程序乃戈,如何才能與指定的應(yīng)用程序通信就要通過(guò) TCP 或 UPD 的地址也就是端口號(hào)來(lái)指定褂痰。這樣就可以通過(guò)一個(gè) Socket 實(shí)例唯一代表一個(gè)主機(jī)上的一個(gè)應(yīng)用程序的通信鏈路了。
建立通信鏈路
當(dāng)客戶端要與服務(wù)端通信症虑,客戶端首先要?jiǎng)?chuàng)建一個(gè) Socket 實(shí)例缩歪,操作系統(tǒng)將為這個(gè) Socket 實(shí)例分配一個(gè)沒(méi)有被使用的本地端口號(hào),并創(chuàng)建一個(gè)包含本地和遠(yuǎn)程地址和端口號(hào)的套接字?jǐn)?shù)據(jù)結(jié)構(gòu)谍憔,這個(gè)數(shù)據(jù)結(jié)構(gòu)將一直保存在系統(tǒng)中直到這個(gè)連接關(guān)閉匪蝙。在創(chuàng)建 Socket 實(shí)例的構(gòu)造函數(shù)正確返回之前主籍,將要進(jìn)行 TCP 的三次握手協(xié)議,TCP 握手協(xié)議完成后骗污,Socket 實(shí)例對(duì)象將創(chuàng)建完成崇猫,否則將拋出 IOException 錯(cuò)誤。
與之對(duì)應(yīng)的服務(wù)端將創(chuàng)建一個(gè) ServerSocket 實(shí)例需忿,ServerSocket 創(chuàng)建比較簡(jiǎn)單只要指定的端口號(hào)沒(méi)有被占用诅炉,一般實(shí)例創(chuàng)建都會(huì)成功,同時(shí)操作系統(tǒng)也會(huì)為 ServerSocket 實(shí)例創(chuàng)建一個(gè)底層數(shù)據(jù)結(jié)構(gòu)屋厘,這個(gè)數(shù)據(jù)結(jié)構(gòu)中包含指定監(jiān)聽(tīng)的端口號(hào)和包含監(jiān)聽(tīng)地址的通配符涕烧,通常情況下都是“*”即監(jiān)聽(tīng)所有地址。之后當(dāng)調(diào)用 accept() 方法時(shí)汗洒,將進(jìn)入阻塞狀態(tài)议纯,等待客戶端的請(qǐng)求。當(dāng)一個(gè)新的請(qǐng)求到來(lái)時(shí)溢谤,將為這個(gè)連接創(chuàng)建一個(gè)新的套接字?jǐn)?shù)據(jù)結(jié)構(gòu)瞻凤,該套接字?jǐn)?shù)據(jù)的信息包含的地址和端口信息正是請(qǐng)求源地址和端口。這個(gè)新創(chuàng)建的數(shù)據(jù)結(jié)構(gòu)將會(huì)關(guān)聯(lián)到 ServerSocket 實(shí)例的一個(gè)未完成的連接數(shù)據(jù)結(jié)構(gòu)列表中世杀,注意這時(shí)服務(wù)端與之對(duì)應(yīng)的 Socket 實(shí)例并沒(méi)有完成創(chuàng)建阀参,而要等到與客戶端的三次握手完成后,這個(gè)服務(wù)端的 Socket 實(shí)例才會(huì)返回瞻坝,并將這個(gè) Socket 實(shí)例對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)從未完成列表中移到已完成列表中蛛壳。所以 ServerSocket 所關(guān)聯(lián)的列表中每個(gè)數(shù)據(jù)結(jié)構(gòu),都代表與一個(gè)客戶端的建立的 TCP 連接所刀。
數(shù)據(jù)傳輸
傳輸數(shù)據(jù)是我們建立連接的主要目的衙荐,如何通過(guò) Socket 傳輸數(shù)據(jù),下面將詳細(xì)介紹浮创。
當(dāng)連接已經(jīng)建立成功忧吟,服務(wù)端和客戶端都會(huì)擁有一個(gè) Socket 實(shí)例,每個(gè) Socket 實(shí)例都有一個(gè) InputStream 和 OutputStream斩披,正是通過(guò)這兩個(gè)對(duì)象來(lái)交換數(shù)據(jù)瀑罗。同時(shí)我們也知道網(wǎng)絡(luò) I/O 都是以字節(jié)流傳輸?shù)摹.?dāng) Socket 對(duì)象創(chuàng)建時(shí)雏掠,操作系統(tǒng)將會(huì)為 InputStream 和 OutputStream 分別分配一定大小的緩沖區(qū)斩祭,數(shù)據(jù)的寫入和讀取都是通過(guò)這個(gè)緩存區(qū)完成的。寫入端將數(shù)據(jù)寫到 OutputStream 對(duì)應(yīng)的 SendQ 隊(duì)列中乡话,當(dāng)隊(duì)列填滿時(shí)摧玫,數(shù)據(jù)將被發(fā)送到另一端 InputStream 的 RecvQ 隊(duì)列中,如果這時(shí) RecvQ 已經(jīng)滿了,那么 OutputStream 的 write 方法將會(huì)阻塞直到 RecvQ 隊(duì)列有足夠的空間容納 SendQ 發(fā)送的數(shù)據(jù)诬像。值得特別注意的是屋群,這個(gè)緩存區(qū)的大小以及寫入端的速度和讀取端的速度非常影響這個(gè)連接的數(shù)據(jù)傳輸效率,由于可能會(huì)發(fā)生阻塞坏挠,所以網(wǎng)絡(luò) I/O 與磁盤 I/O 在數(shù)據(jù)的寫入和讀取還要有一個(gè)協(xié)調(diào)的過(guò)程芍躏,如果兩邊同時(shí)傳送數(shù)據(jù)時(shí)可能會(huì)產(chǎn)生死鎖,在后面 NIO 部分將介紹避免這種情況降狠。
NIO 的工作方式
BIO 帶來(lái)的挑戰(zhàn)
BIO 即阻塞 I/O对竣,不管是磁盤 I/O 還是網(wǎng)絡(luò) I/O,數(shù)據(jù)在寫入 OutputStream 或者從 InputStream 讀取時(shí)都有可能會(huì)阻塞榜配。一旦有線程阻塞將會(huì)失去 CPU 的使用權(quán)否纬,這在當(dāng)前的大規(guī)模訪問(wèn)量和有性能要求情況下是不能接受的。雖然當(dāng)前的網(wǎng)絡(luò) I/O 有一些解決辦法蛋褥,如一個(gè)客戶端一個(gè)處理線程临燃,出現(xiàn)阻塞時(shí)只是一個(gè)線程阻塞而不會(huì)影響其它線程工作,還有為了減少系統(tǒng)線程的開(kāi)銷烙心,采用線程池的辦法來(lái)減少線程創(chuàng)建和回收的成本膜廊,但是有一些使用場(chǎng)景仍然是無(wú)法解決的。如當(dāng)前一些需要大量 HTTP 長(zhǎng)連接的情況淫茵,像淘寶現(xiàn)在使用的 Web 旺旺項(xiàng)目溃论,服務(wù)端需要同時(shí)保持幾百萬(wàn)的 HTTP 連接,但是并不是每時(shí)每刻這些連接都在傳輸數(shù)據(jù)痘昌,這種情況下不可能同時(shí)創(chuàng)建這么多線程來(lái)保持連接。即使線程的數(shù)量不是問(wèn)題炬转,仍然有一些問(wèn)題還是無(wú)法避免的辆苔。如這種情況,我們想給某些客戶端更高的服務(wù)優(yōu)先級(jí)扼劈,很難通過(guò)設(shè)計(jì)線程的優(yōu)先級(jí)來(lái)完成驻啤,另外一種情況是,我們需要讓每個(gè)客戶端的請(qǐng)求在服務(wù)端可能需要訪問(wèn)一些競(jìng)爭(zhēng)資源荐吵,由于這些客戶端是在不同線程中骑冗,因此需要同步,而往往要實(shí)現(xiàn)這些同步操作要遠(yuǎn)遠(yuǎn)比用單線程復(fù)雜很多先煎。以上這些情況都說(shuō)明贼涩,我們需要另外一種新的 I/O 操作方式。
NIO 的工作機(jī)制
很多人都把NIO翻譯成New IO薯蝎,但我更覺(jué)得No-Block IO更接近它的本意遥倦,也就是非阻塞式IO,它雖然是非阻塞式的占锯,但它是同步的袒哥,我們先看一下 NIO 涉及到的關(guān)聯(lián)類圖缩筛,如下:
上圖中有兩個(gè)關(guān)鍵類:Channel 和 Selector,它們是 NIO 中兩個(gè)核心概念堡称。我們還用前面的城市交通工具來(lái)繼續(xù)比喻 NIO 的工作方式瞎抛,這里的 Channel 要比 Socket 更加具體,它可以比作為某種具體的交通工具却紧,如汽車或是高鐵等桐臊,而 Selector 可以比作為一個(gè)車站的車輛運(yùn)行調(diào)度系統(tǒng),它將負(fù)責(zé)監(jiān)控每輛車的當(dāng)前運(yùn)行狀態(tài):是已經(jīng)出戰(zhàn)還是在路上等等啄寡,也就是它可以輪詢每個(gè) Channel 的狀態(tài)豪硅。這里還有一個(gè) Buffer 類,它也比 Stream 更加具體化挺物,我們可以將它比作為車上的座位懒浮,Channel 是汽車的話就是汽車上的座位,高鐵上就是高鐵上的座位识藤,它始終是一個(gè)具體的概念砚著,與 Stream 不同。Stream 只能代表是一個(gè)座位痴昧,至于是什么座位由你自己去想象稽穆,也就是你在去上車之前并不知道,這個(gè)車上是否還有沒(méi)有座位了赶撰,也不知道上的是什么車舌镶,因?yàn)槟悴⒉荒苓x擇,這些信息都已經(jīng)被封裝在了運(yùn)輸工具(Socket)里面了豪娜,對(duì)你是透明的餐胀。
NIO 引入了 Channel、Buffer 和 Selector 就是想把這些信息具體化瘤载,讓程序員有機(jī)會(huì)控制它們否灾,如:當(dāng)我們調(diào)用 write() 往 SendQ 寫數(shù)據(jù)時(shí),當(dāng)一次寫的數(shù)據(jù)超過(guò) SendQ 長(zhǎng)度是需要按照 SendQ 的長(zhǎng)度進(jìn)行分割鸣奔,這個(gè)過(guò)程中需要有將用戶空間數(shù)據(jù)和內(nèi)核地址空間進(jìn)行切換墨技,而這個(gè)切換不是你可以控制的。而在 Buffer 中我們可以控制 Buffer 的 capacity挎狸,并且是否擴(kuò)容以及如何擴(kuò)容都可以控制扣汪。
理解了這些概念后我們看一下,實(shí)際上它們是如何工作的锨匆,下面是典型的一段 NIO 代碼:
public void selector() throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);//設(shè)置為非阻塞方式
ssc.socket().bind(new InetSocketAddress(8080));
ssc.register(selector, SelectionKey.OP_ACCEPT);//注冊(cè)監(jiān)聽(tīng)的事件
while (true) {
Set selectedKeys = selector.selectedKeys();//取得所有key集合
Iterator it = selectedKeys.iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {
ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
SocketChannel sc = ssChannel.accept();//接受到服務(wù)端的請(qǐng)求
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
it.remove();
} else if
((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {
SocketChannel sc = (SocketChannel) key.channel();
while (true) {
buffer.clear();
int n = sc.read(buffer);//讀取數(shù)據(jù)
if (n <= 0) {
break;
}
buffer.flip();
}
it.remove();
}
}
}
}
調(diào)用 Selector 的靜態(tài)工廠創(chuàng)建一個(gè)選擇器私痹,創(chuàng)建一個(gè)服務(wù)端的 Channel 綁定到一個(gè) Socket 對(duì)象,并把這個(gè)通信信道注冊(cè)到選擇器上,把這個(gè)通信信道設(shè)置為非阻塞模式紊遵。然后就可以調(diào)用 Selector 的 selectedKeys 方法來(lái)檢查已經(jīng)注冊(cè)在這個(gè)選擇器上的所有通信信道是否有需要的事件發(fā)生账千,如果有某個(gè)事件發(fā)生時(shí),將會(huì)返回所有的 SelectionKey暗膜,通過(guò)這個(gè)對(duì)象 Channel 方法就可以取得這個(gè)通信信道對(duì)象從而可以讀取通信的數(shù)據(jù)匀奏,而這里讀取的數(shù)據(jù)是 Buffer,這個(gè) Buffer 是我們可以控制的緩沖器学搜。
在上面的這段程序中娃善,是將 Server 端的監(jiān)聽(tīng)連接請(qǐng)求的事件和處理請(qǐng)求的事件放在一個(gè)線程中,但是在實(shí)際應(yīng)用中瑞佩,我們通常會(huì)把它們放在兩個(gè)線程中聚磺,一個(gè)線程專門負(fù)責(zé)監(jiān)聽(tīng)客戶端的連接請(qǐng)求,而且是阻塞方式執(zhí)行的炬丸;另外一個(gè)線程專門來(lái)處理請(qǐng)求瘫寝,這個(gè)專門處理請(qǐng)求的線程才會(huì)真正采用 NIO 的方式,像 Web 服務(wù)器 Tomcat 和 Jetty 都是這個(gè)處理方式稠炬,關(guān)于 Tomcat 和 Jetty 的 NIO 處理方式可以參考文章《 Jetty 的工作原理和與 Tomcat 的比較》焕阿。
下圖是描述了基于 NIO 工作方式的 Socket 請(qǐng)求的處理過(guò)程:
上圖中的 Selector 可以同時(shí)監(jiān)聽(tīng)一組通信信道(Channel)上的 I/O 狀態(tài),前提是這個(gè) Selector 要已經(jīng)注冊(cè)到這些通信信道中首启。選擇器 Selector 可以調(diào)用 select() 方法檢查已經(jīng)注冊(cè)的通信信道上的是否有 I/O 已經(jīng)準(zhǔn)備好暮屡,如果沒(méi)有至少一個(gè)信道 I/O 狀態(tài)有變化,那么 select 方法會(huì)阻塞等待或在超時(shí)時(shí)間后會(huì)返回 0毅桃。上圖中如果有多個(gè)信道有數(shù)據(jù)褒纲,那么將會(huì)將這些數(shù)據(jù)分配到對(duì)應(yīng)的數(shù)據(jù) Buffer 中。所以關(guān)鍵的地方是有一個(gè)線程來(lái)處理所有連接的數(shù)據(jù)交互钥飞,每個(gè)連接的數(shù)據(jù)交互都不是阻塞方式莺掠,所以可以同時(shí)處理大量的連接請(qǐng)求。
Buffer 的工作方式
上面介紹了 Selector 將檢測(cè)到有通信信道 I/O 有數(shù)據(jù)傳輸時(shí)代承,通過(guò) selelct() 取得 SocketChannel,將數(shù)據(jù)讀取或?qū)懭?Buffer 緩沖區(qū)渐扮。下面討論一下 Buffer 如何接受和寫出數(shù)據(jù)论悴?
Buffer 可以簡(jiǎn)單的理解為一組基本數(shù)據(jù)類型的元素列表,它通過(guò)幾個(gè)變量來(lái)保存這個(gè)數(shù)據(jù)的當(dāng)前位置狀態(tài)墓律,也就是有四個(gè)索引膀估。如下表所示:
索引 | 說(shuō)明 |
---|---|
capacity | 緩沖區(qū)數(shù)組的總長(zhǎng)度 |
position | 下一個(gè)要操作的數(shù)據(jù)元素的位置 |
limit | 緩沖區(qū)數(shù)組中不可操作的下一個(gè)元素的位置,limit<=capacity |
mark | 用于記錄當(dāng)前 position 的前一個(gè)位置或者默認(rèn)是 0 |
在實(shí)際操作數(shù)據(jù)時(shí)它們有如下關(guān)系圖:
我們通過(guò) ByteBuffer.allocate(11) 方法創(chuàng)建一個(gè) 11 個(gè) byte 的數(shù)組緩沖區(qū)耻讽,初始狀態(tài)如上圖所示察纯,position 的位置為 0,capacity 和 limit 默認(rèn)都是數(shù)組長(zhǎng)度。當(dāng)我們寫入 5 個(gè)字節(jié)時(shí)位置變化如下圖所示:
這時(shí)底層操作系統(tǒng)就可以從緩沖區(qū)中正確讀取這 5 個(gè)字節(jié)數(shù)據(jù)發(fā)送出去了饼记。在下一次寫數(shù)據(jù)之前我們?cè)谡{(diào)一下 clear() 方法香伴。緩沖區(qū)的索引狀態(tài)又回到初始位置。
這里還要說(shuō)明一下 mark具则,當(dāng)我們調(diào)用 mark() 時(shí)即纲,它將記錄當(dāng)前 position 的前一個(gè)位置,當(dāng)我們調(diào)用 reset 時(shí)博肋,position 將恢復(fù) mark 記錄下來(lái)的值低斋。
還有一點(diǎn)需要說(shuō)明,通過(guò) Channel 獲取的 I/O 數(shù)據(jù)首先要經(jīng)過(guò)操作系統(tǒng)的 Socket 緩沖區(qū)再將數(shù)據(jù)復(fù)制到 Buffer 中匪凡,這個(gè)的操作系統(tǒng)緩沖區(qū)就是底層的 TCP 協(xié)議關(guān)聯(lián)的 RecvQ 或者 SendQ 隊(duì)列膊畴,從操作系統(tǒng)緩沖區(qū)到用戶緩沖區(qū)復(fù)制數(shù)據(jù)比較耗性能,Buffer 提供了另外一種直接操作操作系統(tǒng)緩沖區(qū)的的方式即 ByteBuffer.allocateDirector(size)病游,這個(gè)方法返回的 byteBuffer 就是與底層存儲(chǔ)空間關(guān)聯(lián)的緩沖區(qū)唇跨,它的操作方式與 linux2.4 內(nèi)核的 sendfile 操作方式類似。
Java NIO 實(shí)例
上面從 NIO 中引入了一些概念礁遵,下面我們對(duì)這些概念再來(lái)進(jìn)行簡(jiǎn)單的復(fù)述和補(bǔ)充:
- 緩沖區(qū)Buffer:緩沖區(qū)是一個(gè)對(duì)象轻绞,里面存的是數(shù)據(jù),NIO進(jìn)行通訊佣耐,傳遞的數(shù)據(jù)政勃,都包裝到Buffer中,Buffer是一個(gè)抽象類兼砖。子類有ByteBuffer奸远、CharBuffer等,常用的是字節(jié)緩沖區(qū)讽挟,也就是ByteBuffer懒叛;
- 通道Channel:channel是一個(gè)通道,通道就是通流某種物質(zhì)的管道耽梅,在這里就是通流數(shù)據(jù)薛窥,他和流的不同之處就在于,流是單向的眼姐,只能向一個(gè)方向流動(dòng)诅迷,而通道是一個(gè)管道,有兩端众旗,是雙向的罢杉,可以進(jìn)行讀操作,也可以寫操作贡歧,或者兩者同時(shí)進(jìn)行滩租;
- 多路復(fù)用器Selector:多路復(fù)用器是一個(gè)大管家赋秀,他管理著通道,通道把自己注冊(cè)到Selector上面律想,Selector會(huì)輪詢注冊(cè)到自己的管道猎莲,通過(guò)判斷這個(gè)管道的不同的狀態(tài),來(lái)進(jìn)行相應(yīng)的操作蜘欲;
NIO 工作機(jī)制的核心思想就是:客戶端和服務(wù)器端都是使用的通道益眉,通道具有事件,可以將事件注冊(cè)到多路復(fù)選器上姥份,事件有就緒和非就緒兩種狀態(tài)郭脂,就緒的狀態(tài)會(huì)放到多路復(fù)選器的就緒鍵的集合中,起一個(gè)線程不斷地去輪詢就緒的狀態(tài)澈歉,根據(jù)不同的狀態(tài)做不同的處理
NIO 和 IO 的主要區(qū)別
-
面向流與面向緩沖.
Java NIO和IO之間第一個(gè)最大的區(qū)別是展鸡,IO是面向流的,NIO是面向緩沖區(qū)的埃难。Java IO面向流意味著每次從流中讀一個(gè)或多個(gè)字節(jié)莹弊,直至讀取所有字節(jié),它們沒(méi)有被緩存在任何地方涡尘。此外忍弛,它不能前后移動(dòng)流中的數(shù)據(jù)。如果需要前后移動(dòng)從流中讀取的數(shù)據(jù)考抄,需要先將它緩存到一個(gè)緩沖區(qū)细疚。 Java NIO的緩沖導(dǎo)向方法略有不同。數(shù)據(jù)讀取到一個(gè)它稍后處理的緩沖區(qū)川梅,需要時(shí)可在緩沖區(qū)中前后移動(dòng)疯兼。這就增加了處理過(guò)程中的靈活性。 -
阻塞與非阻塞IO
Java IO的各種流是阻塞的贫途。這意味著吧彪,當(dāng)一個(gè)線程調(diào)用read() 或 write()時(shí),該線程被阻塞丢早,直到有一些數(shù)據(jù)被讀取姨裸,或數(shù)據(jù)完全寫入。該線程在此期間不能再干任何事情了怨酝。 Java NIO的非阻塞模式傀缩,使一個(gè)線程從某通道發(fā)送請(qǐng)求讀取數(shù)據(jù),但是它僅能得到目前可用的數(shù)據(jù)凫碌,如果目前沒(méi)有數(shù)據(jù)可用時(shí)扑毡,該線程可以繼續(xù)做其他的事情胃榕。 非阻塞寫也是如此盛险。一個(gè)線程請(qǐng)求寫入一些數(shù)據(jù)到某通道瞄摊,但不需要等待它完全寫入,這個(gè)線程同時(shí)可以去做別的事情苦掘。線程通常將非阻塞IO的空閑時(shí)間用于在其它通道上執(zhí)行IO操作换帜,所以一個(gè)單獨(dú)的線程現(xiàn)在可以管理多個(gè)輸入和輸出通道(channel)。 -
選擇器(Selectors)
Java NIO的選擇器允許一個(gè)單獨(dú)的線程來(lái)監(jiān)視多個(gè)輸入通道鹤啡,你可以注冊(cè)多個(gè)通道使用一個(gè)選擇器惯驼,然后使用一個(gè)單獨(dú)的線程來(lái)“選擇”通道:這些通道里已經(jīng)有可以處理的輸入,或者選擇已準(zhǔn)備寫入的通道递瑰。這種選擇機(jī)制祟牲,使得一個(gè)單獨(dú)的線程很容易來(lái)管理多個(gè)通道。
Java AIO 簡(jiǎn)單了解
AIO就是異步非阻塞IO抖部,A就是asynchronous的意思说贝,因?yàn)镹IO1.0雖然面向緩沖,利用多路復(fù)選器實(shí)現(xiàn)了同步非阻塞IO慎颗,可是在NIO1.0中需要使用一個(gè)線程不斷去輪詢就緒集合乡恕,開(kāi)銷也是比較大的,所以在jdk1.7中擴(kuò)展了NIO俯萎,稱之為NIO2.0傲宜,NIO2.0中引入了AIO,此外NIO2.0中還引入了異步文件通道夫啊,那么究竟是怎么實(shí)現(xiàn)異步的呢函卒?
AIO 有三個(gè)特點(diǎn),它的特點(diǎn)也可以說(shuō)明它是如何完成異步這樣的操作的:
- ①讀完了再通知我涮母;
- ②不會(huì)加快 I/O谆趾,只是在讀完后進(jìn)行通知;
- ③使用回調(diào)函數(shù)叛本,進(jìn)行業(yè)務(wù)處理沪蓬;
AIO 的核心原理就是:對(duì)客戶端和服務(wù)器端的各種操作進(jìn)行回調(diào)函數(shù)的注冊(cè)(通過(guò)實(shí)現(xiàn)一個(gè)CompletionHandler接口,其中定義了一個(gè)completed的成功操作方法和一個(gè)fail的失敗方法)来候。在完成某個(gè)操作之后跷叉,就會(huì)自己去調(diào)用該注冊(cè)到該操作的回調(diào)函數(shù),達(dá)到異步的效果营搅。
BIO/ NIO/ AIO 的簡(jiǎn)單理解
我們?cè)谶@里假設(shè)一個(gè)燒了一排開(kāi)水的場(chǎng)景云挟,BIO(同步阻塞IO)的做法就是,叫一個(gè)線程停留在一個(gè)水壺那转质,直到這個(gè)水壺?zé)_(kāi)我再去處理下一個(gè)水壺园欣;NIO(準(zhǔn)備好再通知我,同步非阻塞IO)的做法就是叫一個(gè)線程不斷地去詢問(wèn)每個(gè)水壺的狀態(tài)休蟹,看看是否有水壺的狀態(tài)發(fā)生了變化沸枯,變化則再去做相應(yīng)的處理日矫;AIO(讀完了再通知我,異步非阻塞IO)的做法是在每個(gè)水壺上都安裝一個(gè)裝置绑榴,當(dāng)水壺?zé)_(kāi)之后就會(huì)自動(dòng)通知我水壺?zé)_(kāi)了讓我做相應(yīng)的處理哪轿;
如果還覺(jué)得理解起來(lái)有困難的童鞋建議閱讀以下這篇文章,相信會(huì)有收獲:
http://loveshisong.cn/編程技術(shù)/2016-06-25-十分鐘了解BIO-NIO-AIO.html
BIO翔怎、NIO窃诉、AIO適用場(chǎng)景分析
- BIO方式適用于連接數(shù)目比較小且固定的架構(gòu),這種方式對(duì)服務(wù)器資源要求比較高赤套,并發(fā)局限于應(yīng)用中飘痛,JDK1.4以前的唯一選擇,但程序直觀簡(jiǎn)單易理解容握。
- NIO方式適用于連接數(shù)目多且連接比較短(輕操作)的架構(gòu)敦冬,比如聊天服務(wù)器,并發(fā)局限于應(yīng)用中唯沮,編程比較復(fù)雜脖旱,JDK1.4開(kāi)始支持。
- AIO方式使用于連接數(shù)目多且連接比較長(zhǎng)(重操作)的架構(gòu)介蛉,比如相冊(cè)服務(wù)器萌庆,充分調(diào)用OS參與并發(fā)操作,編程比較復(fù)雜币旧,JDK7開(kāi)始支持践险。
簡(jiǎn)單總結(jié)
這篇文章大量復(fù)制粘貼到《深入分析 Java Web 技術(shù)內(nèi)幕》第二節(jié)“深入分析 Java I/O 的工作機(jī)制”的內(nèi)容,沒(méi)辦法確實(shí)很多描述性的概念以及說(shuō)明吹菱,自己的說(shuō)明也沒(méi)有達(dá)到用簡(jiǎn)單語(yǔ)言能描述復(fù)雜事物的程度..所以可能看起來(lái)這篇文章會(huì)有那么點(diǎn)兒難以下咽..我自己的話也是為了寫著一篇文章查了很多資料巍虫,書也是翻了很多很多遍才對(duì)Java 中的 I/O 相關(guān)的知識(shí)有所熟悉,不過(guò)耗費(fèi)的時(shí)間也是值得的鳍刷,同時(shí)也希望觀看文章的你能夠有所收獲占遥,也歡迎各位指正!
歡迎轉(zhuǎn)載输瓜,轉(zhuǎn)載請(qǐng)注明出處瓦胎!
簡(jiǎn)書ID:@我沒(méi)有三顆心臟
github:wmyskxz
歡迎關(guān)注公眾微信號(hào):wmyskxz_javaweb
分享自己的Java Web學(xué)習(xí)之路以及各種Java學(xué)習(xí)資料
想要交流的朋友也可以加qq群:3382693