一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

如果有人再問你 Java IO,就把這篇文章砸他頭上

 hlhq1 2019-12-28

一、簡介

說到 I/O,想必大家都不會陌生, I/O 英語全稱:Input/Output,即輸入/輸出,通常指數(shù)據(jù)在內(nèi)部存儲器和外部存儲器或其他周邊設(shè)備之間的輸入和輸出。

比如我們常用的 SD 卡、U 盤、移動硬盤等等存儲文件的硬件設(shè)備,當(dāng)我們將其插入電腦的 usb 硬件接口時,我們就可以從電腦中讀取設(shè)備中的信息或者寫入信息,這個過程就涉及到 I/O 的操作。

如果有人再問你 Java IO,就把這篇文章砸他頭上

當(dāng)然,涉及 I/O 的操作,不僅僅局限于硬件設(shè)備的讀寫,還要網(wǎng)絡(luò)數(shù)據(jù)的傳輸,比如,我們在電腦上用瀏覽器搜索互聯(lián)網(wǎng)上的信息,這個過程也涉及到 I/O 的操作。

如果有人再問你 Java IO,就把這篇文章砸他頭上

無論是從磁盤中讀寫文件,還是在網(wǎng)絡(luò)中傳輸數(shù)據(jù),可以說 I/O 主要為處理人機(jī)交互機(jī)與機(jī)交互中獲取和交換信息提供的一套解決方案。

在 Java 的 IO 體系中,類將近有 80 個,位于java.io包下,感覺很復(fù)雜,但是這些類大致可以分成四組:

  • 基于字節(jié)操作的 I/O 接口:InputStream 和 OutputStream
  • 基于字符操作的 I/O 接口:Writer 和 Reader
  • 基于磁盤操作的 I/O 接口:File
  • 基于網(wǎng)絡(luò)操作的 I/O 接口:Socket

前兩組主要從傳輸數(shù)據(jù)的數(shù)據(jù)格式不同,進(jìn)行分組;后兩組主要從傳輸數(shù)據(jù)的方式不同,進(jìn)行分組。

雖然 Socket 類并不在java.io包下,但是我們?nèi)匀话阉鼈儎澐衷谝黄?,因?yàn)?I/O 的核心問題,要么是數(shù)據(jù)格式影響 I/O 操作,要么是傳輸方式影響 I/O 操作,也就是將什么樣的數(shù)據(jù)寫到什么地方的問題,I/O 只是人與機(jī)器或者機(jī)器與機(jī)器交互的手段,除了在它們能夠完成這個交互功能外,我們關(guān)注的就是如何提高它的運(yùn)行效率了,而數(shù)據(jù)格式傳輸方式是影響效率最關(guān)鍵的因素。

本文后面,也是基于這兩個點(diǎn)進(jìn)行深入展開分析。

二、基于字節(jié)操作的接口

基于字節(jié)的輸入和輸出操作接口分別是:InputStream 和 OutputStream 。

2.1、字節(jié)輸入流

InputStream 輸入流的類繼承層次如下圖所示:

如果有人再問你 Java IO,就把這篇文章砸他頭上

輸入流根據(jù)數(shù)據(jù)節(jié)點(diǎn)類型和處理方式,分別可以劃分出了若干個子類,如下圖:

如果有人再問你 Java IO,就把這篇文章砸他頭上

OutputStream 輸出流的類層次結(jié)構(gòu)也是類似。

2.2、字節(jié)輸出流

OutputStream 輸出流的類繼承層次如下圖所示:

如果有人再問你 Java IO,就把這篇文章砸他頭上

輸出流根據(jù)數(shù)據(jù)節(jié)點(diǎn)類型和處理方式,也分別可以劃分出了若干個子類,如下圖:

如果有人再問你 Java IO,就把這篇文章砸他頭上

在這里就不詳細(xì)的介紹各個子類的使用方法,有興趣的朋友可以查看 JDK 的 API 說明文檔,筆者也會在后期的文章會進(jìn)行詳細(xì)的介紹,這里只是重點(diǎn)想說一下,無論是輸入還是輸出,操作數(shù)據(jù)的方式可以組合使用,各個處理流的類并不是只操作固定的節(jié)點(diǎn)流,比如如下輸出方式:

//將文件輸出流包裝到序列化輸出流中,再將序列化輸出流包裝到緩沖中OutputStream out = new BufferedOutputStream(new ObjectOutputStream(new FileOutputStream(new File('fileName')));

另外,輸出流最終寫到什么地方必須要指定,要么是寫到硬盤中,要么是寫到網(wǎng)絡(luò)中,從圖中可以發(fā)現(xiàn),寫網(wǎng)絡(luò)實(shí)際上也是寫文件,只不過寫到網(wǎng)絡(luò)中,需要經(jīng)過底層操作系統(tǒng)將數(shù)據(jù)發(fā)送到其他的計(jì)算機(jī)中,而不是寫入到本地硬盤中。

三、基于字符操作的接口

不管是磁盤還是網(wǎng)絡(luò)傳輸,最小的存儲單元都是字節(jié),而不是字符,所以 I/O 操作的都是字節(jié)而不是字符,但是為什么要有操作字符的 I/O 接口呢?

這是因?yàn)槲覀兊某绦蛑型ǔ2僮鞯臄?shù)據(jù)都是以字符形式,為了程序操作更方便而提供一個直接寫字符的 I/O 接口,僅此而已。

基于字符的輸入和輸出操作接口分別是:Reader 和 Writer ,下圖是字符的 I/O 操作接口涉及到的類結(jié)構(gòu)圖。

3.1、字符輸入流

Reader 輸入流的類繼承層次如下圖所示:

如果有人再問你 Java IO,就把這篇文章砸他頭上

同樣的,輸入流根據(jù)數(shù)據(jù)節(jié)點(diǎn)類型和處理方式,分別可以劃分出了若干個子類,如下圖:

如果有人再問你 Java IO,就把這篇文章砸他頭上

3.2、字符輸出流

Writer 輸出流的類繼承層次如下圖所示:

如果有人再問你 Java IO,就把這篇文章砸他頭上

同樣的,輸出流根據(jù)數(shù)據(jù)節(jié)點(diǎn)類型和處理方式分類,分別可以劃分出了若干個子類,如下圖:

如果有人再問你 Java IO,就把這篇文章砸他頭上

不管是 Reader 還是 Writer 類,它們都只定義了讀取或?qū)懭霐?shù)據(jù)字符的方式,也就是說要么是讀要么是寫,但是并沒有規(guī)定數(shù)據(jù)要寫到哪去,寫到哪去就是我們后面要討論的基于磁盤或網(wǎng)絡(luò)的工作機(jī)制。

四、字節(jié)與字符的轉(zhuǎn)化

剛剛我們說到,不管是磁盤還是網(wǎng)絡(luò)傳輸,最小的存儲單元都是字節(jié),而不是字符,設(shè)計(jì)字符的原因是為了程序操作更方便,那么怎么將字符轉(zhuǎn)化成字節(jié)或者將字節(jié)轉(zhuǎn)化成字符呢?

InputStreamReader 和 OutputStreamWriter 就是轉(zhuǎn)化橋梁。

4.1、輸入流轉(zhuǎn)化過程

輸入流字符解碼相關(guān)類結(jié)構(gòu)的轉(zhuǎn)化過程如下圖所示:

如果有人再問你 Java IO,就把這篇文章砸他頭上

從圖上可以看到,InputStreamReader 類是字節(jié)到字符的轉(zhuǎn)化橋梁, 其中StreamDecoder指的是一個解碼操作類,Charset指的是字符集。

InputStream 到 Reader 的過程需要指定編碼字符集,否則將采用操作系統(tǒng)默認(rèn)字符集,很可能會出現(xiàn)亂碼問題,StreamDecoder 則是完成字節(jié)到字符的解碼的實(shí)現(xiàn)類。

打開源碼部分,InputStream 到 Reader 轉(zhuǎn)化過程,如下圖:

如果有人再問你 Java IO,就把這篇文章砸他頭上

4.1、輸出流轉(zhuǎn)化過程

輸出流轉(zhuǎn)化過程也是類似,如下圖所示:

如果有人再問你 Java IO,就把這篇文章砸他頭上

通過 OutputStreamWriter 類完成字符到字節(jié)的編碼過程,由 StreamEncoder 完成編碼過程。

源碼部分,Writer 到 OutputStream 轉(zhuǎn)化過程,如下圖:

如果有人再問你 Java IO,就把這篇文章砸他頭上

五、基于磁盤操作的接口

前面介紹了 Java I/O 的操作接口,這些接口主要定義了如何操作數(shù)據(jù),以及介紹了操作數(shù)據(jù)格式的方式:字節(jié)流和字符流。

還有一個關(guān)鍵問題就是數(shù)據(jù)寫到何處,其中一個主要的處理方式就是將數(shù)據(jù)持久化到物理磁盤。

我們知道數(shù)據(jù)在磁盤的唯一最小描述就是文件,也就是說上層應(yīng)用程序只能通過文件來操作磁盤上的數(shù)據(jù),文件也是操作系統(tǒng)和磁盤驅(qū)動器交互的一個最小單元。

如果有人再問你 Java IO,就把這篇文章砸他頭上

在 Java I/O 體系中,F(xiàn)ile 類是唯一代表磁盤文件本身的對象。

File 類定義了一些與平臺無關(guān)的方法來操作文件,包括檢查一個文件是否存在、創(chuàng)建、刪除文件、重命名文件、判斷文件的讀寫權(quán)限是否存在、設(shè)置和查詢文件的最近修改時間等等操作。

值得注意的是 Java 中通常的 File 并不代表一個真實(shí)存在的文件對象,當(dāng)你通過指定一個路徑描述符時,它就會返回一個代表這個路徑相關(guān)聯(lián)的一個虛擬對象,這個可能是一個真實(shí)存在的文件或者是一個包含多個文件的目錄。

例如,讀取一個文件內(nèi)容,程序如下:

如果有人再問你 Java IO,就把這篇文章砸他頭上

以上面的程序?yàn)槔?,從硬盤中讀取一段文本字符,操作流程如下圖:

如果有人再問你 Java IO,就把這篇文章砸他頭上

我們再來看看源碼執(zhí)行流程。

當(dāng)我們傳入一個指定的文件名來創(chuàng)建 File 對象,通過 FileReader 來讀取文件內(nèi)容時,會自動創(chuàng)建一個FileInputStream對象來讀取文件內(nèi)容,也就是我們上文中所說的字節(jié)流來讀取文件。

如果有人再問你 Java IO,就把這篇文章砸他頭上

緊接著,會創(chuàng)建一個FileDescriptor的對象,其實(shí)這個對象就是真正代表一個存在的文件對象的描述??梢酝ㄟ^FileInputStream對象調(diào)用getFD()方法獲取真正與底層操作系統(tǒng)關(guān)聯(lián)的文件描述。

如果有人再問你 Java IO,就把這篇文章砸他頭上

由于我們需要讀取的是字符格式,所以需要 StreamDecoder 類將byte解碼為char格式,至于如何從磁盤驅(qū)動器上讀取一段數(shù)據(jù),由操作系統(tǒng)幫我們完成。

六、基于網(wǎng)絡(luò)操作的接口

繼續(xù)來說說數(shù)據(jù)寫到何處的另一種處理方式:將數(shù)據(jù)寫入互聯(lián)網(wǎng)中以供其他電腦能訪問。

6.1、Socket 簡介

在現(xiàn)實(shí)中,Socket 這個概念沒有一個具體的實(shí)體,它是描述計(jì)算機(jī)之間完成相互通信一種抽象定義。

打個比方,可以把 Socket 比作為兩個城市之間的交通工具,有了它,就可以在城市之間來回穿梭了。并且,交通工具有多種,每種交通工具也有相應(yīng)的交通規(guī)則。Socket 也一樣,也有多種。大部分情況下我們使用的都是基于 TCP/IP 的流套接字,它是一種穩(wěn)定的通信協(xié)議。

典型的基于 Socket 通信的應(yīng)用程序場景,如下圖:

如果有人再問你 Java IO,就把這篇文章砸他頭上

主機(jī) A 的應(yīng)用程序要想和主機(jī) B 的應(yīng)用程序通信,必須通過 Socket 建立連接,而建立 Socket 連接必須需要底層 TCP/IP 協(xié)議來建立 TCP 連接。

6.2、建立通信鏈路

我們知道網(wǎng)絡(luò)層使用的 IP 協(xié)議可以幫助我們根據(jù) IP 地址來找到目標(biāo)主機(jī),但是一臺主機(jī)上可能運(yùn)行著多個應(yīng)用程序,如何才能與指定的應(yīng)用程序通信就要通過 TCP 或 UPD 的地址也就是端口號來指定。這樣就可以通過一個 Socket 實(shí)例代表唯一一個主機(jī)上的一個應(yīng)用程序的通信鏈路了。

為了準(zhǔn)確無誤地把數(shù)據(jù)送達(dá)目標(biāo)處,TCP 協(xié)議采用了三次握手策略,如下圖:

如果有人再問你 Java IO,就把這篇文章砸他頭上

其中,SYN 全稱為 Synchronize Sequence Numbers,表示同步序列編號,是 TCP/IP 建立連接時使用的握手信號。

ACK 全稱為 Acknowledge character,即確認(rèn)字符,表示發(fā)來的數(shù)據(jù)已確認(rèn)接收無誤。

在客戶機(jī)和服務(wù)器之間建立正常的 TCP 網(wǎng)絡(luò)連接時,客戶機(jī)首先發(fā)出一個 SYN 消息,服務(wù)器使用 SYN + ACK 應(yīng)答表示接收到了這個消息,最后客戶機(jī)再以 ACK 消息響應(yīng)。

這樣在客戶機(jī)和服務(wù)器之間才能建立起可靠的 TCP 連接,數(shù)據(jù)才可以在客戶機(jī)和服務(wù)器之間傳遞。

簡單流程如下:

  • 發(fā)送端 –(發(fā)送帶有 SYN 標(biāo)志的數(shù)據(jù)包 )–> 接受端(第一次握手);
  • 接受端 –(發(fā)送帶有 SYN + ACK 標(biāo)志的數(shù)據(jù)包)–> 發(fā)送端(第二次握手);
  • 發(fā)送端 –(發(fā)送帶有 ACK 標(biāo)志的數(shù)據(jù)包) –> 接受端(第三次握手);

完成三次握手之后,客戶端應(yīng)用程序與服務(wù)器應(yīng)用程序就可以開始傳送數(shù)據(jù)了。

傳輸數(shù)據(jù)是我們建立連接的主要目的,如何通過 Socket 傳輸數(shù)據(jù)呢?

6.3、傳輸數(shù)據(jù)

當(dāng)客戶端要與服務(wù)端通信時,客戶端首先要創(chuàng)建一個 Socket 實(shí)例,默認(rèn)操作系統(tǒng)將為這個 Socket 實(shí)例分配一個沒有被使用的本地端口號,并創(chuàng)建一個包含本地、遠(yuǎn)程地址和端口號的套接字?jǐn)?shù)據(jù)結(jié)構(gòu),這個數(shù)據(jù)結(jié)構(gòu)將一直保存在系統(tǒng)中直到這個連接關(guān)閉。

如果有人再問你 Java IO,就把這篇文章砸他頭上

與之對應(yīng)的服務(wù)端,也將創(chuàng)建一個 ServerSocket 實(shí)例,ServerSocket 創(chuàng)建比較簡單,只要指定的端口號沒有被占用,一般實(shí)例創(chuàng)建都會成功,同時操作系統(tǒng)也會為 ServerSocket 實(shí)例創(chuàng)建一個底層數(shù)據(jù)結(jié)構(gòu),這個數(shù)據(jù)結(jié)構(gòu)中包含指定監(jiān)聽的端口號和包含監(jiān)聽地址的通配符,通常情況下都是*即監(jiān)聽所有地址。

之后當(dāng)調(diào)用 accept() 方法時,將進(jìn)入阻塞狀態(tài),等待客戶端的請求。

如果有人再問你 Java IO,就把這篇文章砸他頭上

我們先啟動服務(wù)端程序,再運(yùn)行客戶端,服務(wù)端收到客戶端發(fā)送的信息,服務(wù)端打印結(jié)果如下:

如果有人再問你 Java IO,就把這篇文章砸他頭上

注意,客戶端只有與服務(wù)端建立三次握手成功之后,才會發(fā)送數(shù)據(jù),而 TCP/IP 握手過程,底層操作系統(tǒng)已經(jīng)幫我們實(shí)現(xiàn)了!

當(dāng)連接已經(jīng)建立成功,服務(wù)端和客戶端都會擁有一個 Socket 實(shí)例,每個 Socket 實(shí)例都有一個 InputStreamOutputStream,正如我們前面所說的,網(wǎng)絡(luò) I/O 都是以字節(jié)流傳輸?shù)模琒ocket 正是通過這兩個對象來交換數(shù)據(jù)。

當(dāng) Socket 對象創(chuàng)建時,操作系統(tǒng)將會為 InputStream 和 OutputStream 分別分配一定大小的緩沖區(qū),數(shù)據(jù)的寫入和讀取都是通過這個緩存區(qū)完成的。

寫入端將數(shù)據(jù)寫到 OutputStream 對應(yīng)的 SendQ 隊(duì)列中,當(dāng)隊(duì)列填滿時,數(shù)據(jù)將被發(fā)送到另一端 InputStream 的 RecvQ 隊(duì)列中,如果這時 RecvQ 已經(jīng)滿了,那么 OutputStream 的 write 方法將會阻塞直到 RecvQ 隊(duì)列有足夠的空間容納 SendQ 發(fā)送的數(shù)據(jù)。

值得特別注意的是,緩存區(qū)的大小以及寫入端的速度和讀取端的速度非常影響這個連接的數(shù)據(jù)傳輸效率,由于可能會發(fā)生阻塞,所以網(wǎng)絡(luò) I/O 與磁盤 I/O 在數(shù)據(jù)的寫入和讀取還要有一個協(xié)調(diào)的過程,如果兩邊同時傳送數(shù)據(jù)時可能會產(chǎn)生死鎖的問題。

如何提高網(wǎng)絡(luò) IO 傳輸效率、保證數(shù)據(jù)傳輸?shù)目煽?,已?jīng)成了工程師們急需解決的問題。

6.4、IO 工作方式

在計(jì)算機(jī)中,IO 傳輸數(shù)據(jù)有三種工作方式,分別是 BIO、NIO、AIO。

在講解 BIO、NIO、AIO 之前,我們先來回顧一下這幾個概念:同步與異步,阻塞與非阻塞。

同步與異步的區(qū)別

  • 同步就是發(fā)起一個請求后,接受者未處理完請求之前,不返回結(jié)果。
  • 異步就是發(fā)起一個請求后,立刻得到接受者的回應(yīng)表示已接收到請求,但是接受者并沒有處理完,接受者通常依靠事件回調(diào)等機(jī)制來通知請求者其處理結(jié)果。

阻塞和非阻塞的區(qū)別

  • 阻塞就是請求者發(fā)起一個請求,一直等待其請求結(jié)果返回,也就是當(dāng)前線程會被掛起,無法從事其他任務(wù),只有當(dāng)條件就緒才能繼續(xù)。
  • 非阻塞就是請求者發(fā)起一個請求,不用一直等著結(jié)果返回,可以先去干其他事情,當(dāng)條件就緒的時候,就自動回來。

而我們要講的 BIO、NIO、AIO 就是同步與異步、阻塞與非阻塞的組合。

  • BIO:同步阻塞 IO;
  • NIO:同步非阻塞 IO;
  • AIO:異步非阻塞 IO;

6.4.1、BIO

BIO 俗稱同步阻塞 IO,一種非常傳統(tǒng)的 IO 模型,比如我們上面所舉的那個程序例子,就是一個典型的**同步阻塞 IO **的工作方式。

如果有人再問你 Java IO,就把這篇文章砸他頭上

采用 BIO 通信模型的服務(wù)端,通常由一個獨(dú)立的 Acceptor 線程負(fù)責(zé)監(jiān)聽客戶端的連接。

我們一般在服務(wù)端通過while(true)循環(huán)中會調(diào)用accept()方法等待監(jiān)聽客戶端的連接,一旦接收到一個連接請求,就可以建立通信套接字進(jìn)行讀寫操作,此時不能再接收其他客戶端連接請求,只能等待同當(dāng)前連接的客戶端的操作執(zhí)行完成, 不過可以通過多線程來支持多個客戶端的連接。

客戶端多線程操作,程序如下:

如果有人再問你 Java IO,就把這篇文章砸他頭上

服務(wù)端多線程操作,程序如下:

如果有人再問你 Java IO,就把這篇文章砸他頭上

服務(wù)端運(yùn)行結(jié)果,如下:

如果有人再問你 Java IO,就把這篇文章砸他頭上

如果要讓 BIO 通信模型能夠同時處理多個客戶端請求,就必須使用多線程,也就是說它在接收到客戶端連接請求之后為每個客戶端創(chuàng)建一個新的線程進(jìn)行鏈路處理,處理完成之后,通過輸出流返回應(yīng)答給客戶端,線程銷毀。

這就是典型的一請求一應(yīng)答通信模型 。

如果出現(xiàn) 100、1000、甚至 10000 個用戶同時訪問服務(wù)器,這個時候,如果使用這種模型,那么服務(wù)端也會創(chuàng)建與之相同的線程數(shù)量,線程數(shù)急劇膨脹可能會導(dǎo)致線程堆棧溢出、創(chuàng)建新線程失敗等問題,最終導(dǎo)致進(jìn)程宕機(jī)或者僵死,不能對外提供服務(wù)

當(dāng)然,我們可以通過使用 Java 中 ThreadPoolExecutor 線程池機(jī)制來改善,讓線程的創(chuàng)建和回收成本相對較低,保證了系統(tǒng)有限的資源的控制,實(shí)現(xiàn)了 N (客戶端請求數(shù)量)大于 M (處理客戶端請求的線程數(shù)量)的偽異步 I/O 模型。

6.4.2、偽異步 BIO

為了解決同步阻塞 I/O 面臨的一個鏈路需要一個線程處理的問題,后來有人對它的線程模型進(jìn)行了優(yōu)化,后端通過一個線程池來處理多個客戶端的請求接入,形成客戶端個數(shù) M:線程池最大線程數(shù) N 的比例關(guān)系,其中 M 可以遠(yuǎn)遠(yuǎn)大于 N,通過線程池可以靈活地調(diào)配線程資源,設(shè)置線程的最大值,防止由于海量并發(fā)接入導(dǎo)致資源耗盡。

偽異步 IO 模型圖,如下圖:

如果有人再問你 Java IO,就把這篇文章砸他頭上

采用線程池和任務(wù)隊(duì)列可以實(shí)現(xiàn)一種叫做偽異步的 I/O 通信框架,當(dāng)有新的客戶端接入時,將客戶端的 Socket 封裝成一個 Task 投遞到后端的線程池中進(jìn)行處理。

Java 的線程池維護(hù)一個消息隊(duì)列和 N 個活躍線程,對消息隊(duì)列中的任務(wù)進(jìn)行處理。

客戶端,程序如下:

如果有人再問你 Java IO,就把這篇文章砸他頭上

服務(wù)端,程序如下:

如果有人再問你 Java IO,就把這篇文章砸他頭上

先啟動服務(wù)端程序,再啟動客戶端程序,看看運(yùn)行結(jié)果!

服務(wù)端,運(yùn)行結(jié)果如下:

如果有人再問你 Java IO,就把這篇文章砸他頭上

客戶端,運(yùn)行結(jié)果如下:

如果有人再問你 Java IO,就把這篇文章砸他頭上

本例中測試的客戶端數(shù)量是 30,服務(wù)端使用 java 線程池來處理任務(wù),線程數(shù)量為 5 個,服務(wù)端不用為每個客戶端都創(chuàng)建一個線程,由于線程池可以設(shè)置消息隊(duì)列的大小和最大線程數(shù),因此,它的資源占用是可控的,無論多少個客戶端并發(fā)訪問,都不會導(dǎo)致資源的耗盡和宕機(jī)。

在活動連接數(shù)不是特別高的情況下,這種模型是還不錯,可以讓每一個連接專注于自己的 I/O 并且編程模型簡單,也不用過多考慮系統(tǒng)的過載、限流等問題。

但是,它的底層仍然是同步阻塞的 BIO 模型,當(dāng)面對十萬甚至百萬級連接的時候,傳統(tǒng)的 BIO 模型真的是無能為力的,我們需要一種更高效的 I/O 處理模型來應(yīng)對更高的并發(fā)量。

6.4.3、NIO

NIO 中的 N 可以理解為 Non-blocking,一種同步非阻塞的 I/O 模型,在 Java 1.4 中引入,對應(yīng)的在java.nio包下。

NIO 新增了 Channel、Selector、Buffer 等抽象概念,支持面向緩沖、基于通道的 I/O 操作方法。

NIO 提供了與傳統(tǒng) BIO 模型中的 Socket 和 ServerSocket 相對應(yīng)的 SocketChannel和 ServerSocketChannel 兩種不同的套接字通道實(shí)現(xiàn)。

NIO 這兩種通道都支持阻塞和非阻塞兩種模式。阻塞模式使用就像傳統(tǒng)中的支持一樣,比較簡單,但是性能和可靠性都不好;非阻塞模式正好與之相反。

對于低負(fù)載、低并發(fā)的應(yīng)用程序,可以使用同步阻塞 I/O 來提升開發(fā)效率和更好的維護(hù)性;對于高負(fù)載、高并發(fā)的(網(wǎng)絡(luò))應(yīng)用,應(yīng)使用 NIO 的非阻塞模式來開發(fā)。

我們先看一下 NIO 涉及到的核心關(guān)聯(lián)類圖,如下:

如果有人再問你 Java IO,就把這篇文章砸他頭上

上圖中有三個關(guān)鍵類:Channel 、Selector 和 Buffer,它們是 NIO 中的核心概念。

  • Channel:可以理解為通道;
  • Selector:可以理解為選擇器;
  • Buffer:可以理解為數(shù)據(jù)緩沖流;

我們還是用前面的城市交通工具來繼續(xù)形容 NIO 的工作方式,這里的 Channel 要比 Socket 更加具體,它可以比作為某種具體的交通工具,如汽車或是高鐵、飛機(jī)等,而 Selector 可以比作為一個車站的車輛運(yùn)行調(diào)度系統(tǒng),它將負(fù)責(zé)監(jiān)控每輛車的當(dāng)前運(yùn)行狀態(tài):是已經(jīng)出站還是在路上等等,也就是說它可以輪詢每個 Channel 的狀態(tài)。

還有一個 Buffer 類,你可以將它看作為 IO 中 Stream,但是它比 IO 中的 Stream 更加具體化,我們可以將它比作為車上的座位,Channel 如果是汽車的話,那么 Buffer 就是汽車上的座位,Channel 如果是高鐵上,那么 Buffer 就是高鐵上的座位,它始終是一個具體的概念,這一點(diǎn)與 Stream 不同。

Socket 中的 Stream 只能代表是一個座位,至于是什么座位由你自己去想象,也就是說你在上車之前并不知道這個車上是否還有沒有座位,也不知道上的是什么車,因?yàn)槟悴⒉荒苓x擇,這些信息都已經(jīng)被封裝在了運(yùn)輸工具(Socket)里面了。

NIO 引入了 Channel、Buffer 和 Selector 就是想把 IO 傳輸過程中涉及到的信息具體化,讓程序員有機(jī)會去控制它們。

當(dāng)我們進(jìn)行傳統(tǒng)的網(wǎng)絡(luò) IO 操作時,比如調(diào)用 write() 往 Socket 中的 SendQ 隊(duì)列寫數(shù)據(jù)時,當(dāng)一次寫的數(shù)據(jù)超過 SendQ 長度時,操作系統(tǒng)會按照 SendQ 的長度進(jìn)行分割的,這個過程中需要將用戶空間數(shù)據(jù)和內(nèi)核地址空間進(jìn)行切換,而這個切換不是程序員可以控制的,由底層操作系統(tǒng)來幫我們處理。

而在 Buffer 中,我們可以控制 Buffer 的 capacity(容量),并且是否擴(kuò)容以及如何擴(kuò)容都可以控制。

理解了這些概念后我們看一下,實(shí)際上它們是如何工作的呢?

還是以上面的操作為例子,為了方便觀看結(jié)果,本次的客戶端線程請求數(shù)改成 15 個。

客戶端,程序如下:

如果有人再問你 Java IO,就把這篇文章砸他頭上

服務(wù)端,程序如下:

如果有人再問你 Java IO,就把這篇文章砸他頭上

先啟動服務(wù)端程序,再啟動客戶端程序,看看運(yùn)行結(jié)果!

服務(wù)端,運(yùn)行結(jié)果如下:

如果有人再問你 Java IO,就把這篇文章砸他頭上

客戶端,運(yùn)行結(jié)果如下:

如果有人再問你 Java IO,就把這篇文章砸他頭上

當(dāng)然,客戶端也不僅僅只限制于 IO 的寫法,還可以使用SocketChannel來操作客戶端,程序如下:

如果有人再問你 Java IO,就把這篇文章砸他頭上

一樣的,先啟動服務(wù)端,再啟動客戶端,客戶端運(yùn)行結(jié)果如下:

如果有人再問你 Java IO,就把這篇文章砸他頭上

從操作上可以看到,NIO 的操作比傳統(tǒng)的 IO 操作要復(fù)雜的多!

Selector 被稱為選擇器 ,當(dāng)然你也可以翻譯為多路復(fù)用器 。它是 Java NIO 核心組件中的一個,用于檢查一個或多個 Channel(通道)的狀態(tài)是否處于連接就緒接受就緒、可讀就緒可寫就緒。

如此可以實(shí)現(xiàn)單線程管理多個 channels,也就是可以管理多個網(wǎng)絡(luò)連接。

如果有人再問你 Java IO,就把這篇文章砸他頭上

使用 Selector 的好處在于: 相比傳統(tǒng)方式使用多個線程來管理 IO,Selector 使用了更少的線程就可以處理通道了,并且實(shí)現(xiàn)網(wǎng)絡(luò)高效傳輸!

雖然 java 中的 nio 傳輸比較快,為什么大家都不愿意用 JDK 原生 NIO 進(jìn)行開發(fā)呢?

從上面的代碼中大家都可以看出來,除了編程復(fù)雜、編程模型難之外,還有幾個讓人詬病的問題:

  • JDK 的 NIO 底層由 epoll 實(shí)現(xiàn),該實(shí)現(xiàn)飽受詬病的空輪詢 bug 會導(dǎo)致 cpu 飆升 100%!
  • 項(xiàng)目龐大之后,自行實(shí)現(xiàn)的 NIO 很容易出現(xiàn)各類 bug,維護(hù)成本較高!

但是,Google 的 Netty 框架的出現(xiàn),很大程度上改善了 JDK 原生 NIO 所存在的一些讓人難以忍受的問題,關(guān)于 Netty 框架,會在后期的文章里進(jìn)行介紹。

6.4.4、AIO

最后就是 AIO 了,全稱 Asynchronous I/O,可以理解為異步 IO,也被稱為 NIO 2,在 Java 7 中引入了 NIO 的改進(jìn)版 NIO 2,它是異步非阻塞的 IO 模型,也就是我們現(xiàn)在所說的 AIO。

異步 IO 是基于事件和回調(diào)機(jī)制實(shí)現(xiàn)的,也就是應(yīng)用操作之后會直接返回,不會堵塞在那里,當(dāng)后臺處理完成,操作系統(tǒng)會通知相應(yīng)的線程進(jìn)行后續(xù)的操作。

客戶端,程序示例:

如果有人再問你 Java IO,就把這篇文章砸他頭上

服務(wù)端,程序示例:

如果有人再問你 Java IO,就把這篇文章砸他頭上

同樣的,先啟動服務(wù)端程序,再啟動客戶端程序,看看運(yùn)行結(jié)果!

服務(wù)端,運(yùn)行結(jié)果如下:

如果有人再問你 Java IO,就把這篇文章砸他頭上

客戶端端,運(yùn)行結(jié)果如下:

如果有人再問你 Java IO,就把這篇文章砸他頭上

這種組合方式用起來比較復(fù)雜,只有在一些非常復(fù)雜的分布式情況下使用,像集群之間的消息同步機(jī)制一般用這種 I/O 組合方式。如 Cassandra 的 Gossip 通信機(jī)制就是采用異步非阻塞的方式。

Netty 之前也嘗試使用過 AIO,不過又放棄了!

七、總結(jié)

本文闡述的內(nèi)容較多,從 Java 基本 I/O 類庫結(jié)構(gòu)開始說起,主要介紹了 IO 的傳輸格式傳輸方式,以及磁盤 I/O 和網(wǎng)絡(luò) I/O 的基本工作方式。

本篇文章主要對 Java 的 IO 體系以及計(jì)算機(jī)部分網(wǎng)絡(luò)基礎(chǔ)知識做了些簡單的介紹,其實(shí)每一個模塊涉及到的知識都非常非常多,在后期的文章中,會對各個模塊進(jìn)行詳細(xì)的介紹,如果有理解不到的位置,歡迎指出!

覺得文章不錯就給小老弟點(diǎn)個關(guān)注吧,更多內(nèi)容陸續(xù)奉上。

最后,分享一份面試寶典《Java核心知識點(diǎn)整理.pdf》,覆蓋了JVM、鎖、高并發(fā)、反射、Spring原理、微服務(wù)、Zookeeper、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。私信回復(fù)“資料”獲取免費(fèi)領(lǐng)取方式。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    男女午夜福利院在线观看| 日本人妻熟女一区二区三区| 激情亚洲一区国产精品久久| 日韩一区二区三区在线欧洲| 亚洲熟妇av一区二区三区色堂| 国产色第一区不卡高清| 国产精品美女午夜视频| 国产精品自拍杆香蕉视频| 国产免费黄片一区二区| 欧美日韩免费观看视频| 婷婷一区二区三区四区| 日韩专区欧美中文字幕| 国产视频在线一区二区| 国产精品涩涩成人一区二区三区| 东京热电东京热一区二区三区| 少妇丰满a一区二区三区| 亚洲欧美日韩熟女第一页| 久久精品中文字幕人妻中文| 久久热在线视频免费观看| 欧美日韩一区二区综合| 国产日韩欧美在线播放| 人妻一区二区三区多毛女| 中文字幕禁断介一区二区| 久久久精品日韩欧美丰满| 日韩性生活视频免费在线观看| 国产精品一区二区三区日韩av | 精品al亚洲麻豆一区| 日韩欧美一区二区不卡视频| 日韩成人h视频在线观看| 午夜传媒视频免费在线观看| 日韩人妻欧美一区二区久久| 国产精品免费不卡视频| 好东西一起分享老鸭窝| 色婷婷国产熟妇人妻露脸| 亚洲午夜精品视频在线| 国产女高清在线看免费观看| 国产欧美一区二区另类精品| 久久这里只精品免费福利| 久久99精品日韩人妻| 成年人黄片大全在线观看| 精品熟女少妇一区二区三区|