溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

什么事IO流

發(fā)布時(shí)間:2021-10-28 15:53:34 來源:億速云 閱讀:146 作者:iii 欄目:編程語言

本篇內(nèi)容介紹了“什么事IO流”的有關(guān)知識(shí),在實(shí)際案例的操作過程中,不少人都會(huì)遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

傳統(tǒng)的 BIO

Java IO流是一個(gè)龐大的生態(tài)環(huán)境,其內(nèi)部提供了很多不同的輸入流和輸出流,細(xì)分下去還有字節(jié)流和字符流,甚至還有緩沖流提高 IO  性能,轉(zhuǎn)換流將字節(jié)流轉(zhuǎn)換為字符流······看到這些就已經(jīng)對(duì) IO 產(chǎn)生恐懼了,在日常開發(fā)中少不了對(duì)文件的 IO 操作,雖然 apache 已經(jīng)提供了  Commons IO 這種封裝好的組件,但面對(duì)特殊場景時(shí),我們?nèi)孕枰约喝シ庋b一個(gè)高性能的文件 IO 工具類,本文將會(huì)解析 Java IO  中涉及到的各個(gè)類,以及講解如何正確、高效地使用它們。

BIO NIO 和 AIO 的區(qū)別

我們會(huì)以一個(gè)經(jīng)典的燒開水的例子通俗地講解它們之間的區(qū)別

類型燒開水
BIO一直監(jiān)測著某個(gè)水壺,該水壺?zé)_水后再監(jiān)測下一個(gè)水壺
NIO每隔一段時(shí)間就看看所有水壺的狀態(tài),哪個(gè)水壺?zé)_水就去處理哪個(gè)水壺
AIO不用監(jiān)測水壺,每個(gè)水壺?zé)_水后都會(huì)主動(dòng)通知線程說:“我的水燒開了,來處理我吧”

BIO (同步阻塞 I/O)

這里假設(shè)一個(gè)燒開水的場景,有一排水壺在燒開水,BIO的工作模式就是,  小菠蘿一直看著著這個(gè)水壺,直到這個(gè)水壺?zé)_,才去處理下一個(gè)水壺。線程在等待水壺?zé)_的時(shí)間段什么都沒有做。

NIO(同步非阻塞 I/O)

還拿燒開水來說,NIO的做法是小菠蘿一邊玩著手機(jī),每隔一段時(shí)間就看一看每個(gè)水壺的狀態(tài),看看是否有水壺的狀態(tài)發(fā)生了改變,如果某個(gè)水壺?zé)_了,可以先處理那個(gè)水壺,然后繼續(xù)玩手機(jī),繼續(xù)隔一段時(shí)間又看看每個(gè)水壺的狀態(tài)。

AIO (異步非阻塞 I/O)

小菠蘿覺得每隔一段時(shí)間就去看一看水壺太費(fèi)勁了,于是購買了一批燒開水時(shí)可以嗶嗶響的水壺,于是開始燒水后,小菠蘿就直接去客廳玩手機(jī)了,水燒開時(shí),就發(fā)出“嗶嗶”的響聲,通知小菠蘿來關(guān)掉水壺。

什么是流

知識(shí)科普:我們知道任何一個(gè)文件都是以二進(jìn)制形式存在于設(shè)備中,計(jì)算機(jī)就只有 0  和1,你能看見的東西全部都是由這兩個(gè)數(shù)字組成,你看這篇文章時(shí),這篇文章也是由01組成,只不過這些二進(jìn)制串經(jīng)過各種轉(zhuǎn)換演變成一個(gè)個(gè)文字、一張張圖片躍然屏幕上。

而流就是將這些二進(jìn)制串在各種設(shè)備之間進(jìn)行傳輸,如果你覺得有些抽象,我舉個(gè)例子就會(huì)好理解一些:

“下圖是一張圖片,它由01串組成,我們可以通過程序把一張圖片拷貝到一個(gè)文件夾中,

把圖片轉(zhuǎn)化成二進(jìn)制數(shù)據(jù)集,把數(shù)據(jù)一點(diǎn)一點(diǎn)地傳遞到文件夾中 , 類似于水的流動(dòng) , 這樣整體的數(shù)據(jù)就是一個(gè)數(shù)據(jù)流”

什么事IO流

IO 流讀寫數(shù)據(jù)的特點(diǎn):

  • 順序讀寫。讀寫數(shù)據(jù)時(shí),大部分情況下都是按照順序讀寫,讀取時(shí)從文件開頭的第一個(gè)字節(jié)到最后一個(gè)字節(jié),寫出時(shí)也是也如此(RandomAccessFile  可以實(shí)現(xiàn)隨機(jī)讀寫)

  • 字節(jié)數(shù)組。讀寫數(shù)據(jù)時(shí)本質(zhì)上都是對(duì)字節(jié)數(shù)組做讀取和寫出操作,即使是字符流,也是在字節(jié)流基礎(chǔ)上轉(zhuǎn)化為一個(gè)個(gè)字符,所以字節(jié)數(shù)組是 IO  流讀寫數(shù)據(jù)的本質(zhì)。

流的分類

根據(jù)數(shù)據(jù)流向不同分類:輸入流 和 輸出流

  • 輸入流:從磁盤或者其它設(shè)備中將數(shù)據(jù)輸入到進(jìn)程中

  • 輸出流:將進(jìn)程中的數(shù)據(jù)輸出到磁盤或其它設(shè)備上保存

什么事IO流

圖示中的硬盤只是其中一種設(shè)備,還有非常多的設(shè)備都可以應(yīng)用在IO流中,例如:打印機(jī)、硬盤、顯示器、手機(jī)······

根據(jù)處理數(shù)據(jù)的基本單位不同分類:字節(jié)流 和 字符流

  • 字節(jié)流:以字節(jié)(8 bit)為單位做數(shù)據(jù)的傳輸

  • 字符流:以字符為單位(1字符 = 2字節(jié))做數(shù)據(jù)的傳輸

“字符流的本質(zhì)也是通過字節(jié)流讀取,Java 中的字符采用 Unicode  標(biāo)準(zhǔn),在讀取和輸出的過程中,通過以字符為單位,查找對(duì)應(yīng)的碼表將字節(jié)轉(zhuǎn)換為對(duì)應(yīng)的字符?!?/p>

面對(duì)字節(jié)流和字符流,很多讀者都有疑惑:什么時(shí)候需要用字節(jié)流,什么時(shí)候又要用字符流?

我這里做一個(gè)簡單的概括,你可以按照這個(gè)標(biāo)準(zhǔn)去使用:

字符流只針對(duì)字符數(shù)據(jù)進(jìn)行傳輸,所以如果是文本數(shù)據(jù),優(yōu)先采用字符流傳輸;除此之外,其它類型的數(shù)據(jù)(圖片、音頻等),最好還是以字節(jié)流傳輸。

根據(jù)這兩種不同的分類,我們就可以做出下面這個(gè)表格,里面包含了 IO 中最核心的 4 個(gè)頂層抽象類:

數(shù)據(jù)流向 / 數(shù)據(jù)類型字節(jié)流字符流
輸入流InputStreamReader
輸出流OutputStreamWriter

現(xiàn)在看 IO 是不是有一些思路了,不會(huì)覺得很混亂了,我們來看這四個(gè)類下的所有成員。

什么事IO流

[來自于 cxuan 的 《Java基礎(chǔ)核心總結(jié)》]

看到這么多的類是不是又開始覺得混亂了,不要慌,字節(jié)流和字符流下的輸入流和輸出流大部分都是一一對(duì)應(yīng)的,有了上面的表格支撐,我們不需要再擔(dān)心看見某個(gè)類會(huì)懵逼的情況了。

看到 Stream 就知道是字節(jié)流,看到 Reader / Writer 就知道是字符流。

這里還要額外補(bǔ)充一點(diǎn):Java IO 提供了字節(jié)流轉(zhuǎn)換為字符流的轉(zhuǎn)換類,稱為轉(zhuǎn)換流。

轉(zhuǎn)換流 / 數(shù)據(jù)類型字節(jié)流與字符流之間的轉(zhuǎn)換
(輸入)字節(jié)流 => 字符流InputStreamReader
(輸出)字符流 => 字節(jié)流OutputStreamWriter

注意字節(jié)流與字符流之間的轉(zhuǎn)換是有嚴(yán)格定義的:

  • 輸入流:可以將字節(jié)流 => 字符流

  • 輸出流:可以將字符流 => 字節(jié)流

為什么在輸入流不能字符流 => 字節(jié)流,輸出流不能字節(jié)流 => 字符流?

“在存儲(chǔ)設(shè)備上,所有數(shù)據(jù)都是以字節(jié)為單位存儲(chǔ)的,所以輸入到內(nèi)存時(shí)必定是以字節(jié)為單位輸入,輸出到存儲(chǔ)設(shè)備時(shí)必須是以字節(jié)為單位輸出,字節(jié)流才是計(jì)算機(jī)最根本的存儲(chǔ)方式,而字符流是在字節(jié)流的基礎(chǔ)上對(duì)數(shù)據(jù)進(jìn)行轉(zhuǎn)換,輸出字符,但每個(gè)字符依舊是以字節(jié)為單位存儲(chǔ)的。”

節(jié)點(diǎn)流和處理流

在這里需要額外插入一個(gè)小節(jié)講解節(jié)點(diǎn)流和處理流。

  • 節(jié)點(diǎn)流:節(jié)點(diǎn)流是真正傳輸數(shù)據(jù)的流對(duì)象,用于向特定的一個(gè)地方(節(jié)點(diǎn))讀寫數(shù)據(jù),稱為節(jié)點(diǎn)流。例如 FileInputStream

  • 處理流:處理流是對(duì)節(jié)點(diǎn)流的封裝,使用外層的處理流讀寫數(shù)據(jù),本質(zhì)上是利用節(jié)點(diǎn)流的功能,外層的處理流可以提供額外的功能。處理流的基類都是以 Filter  開頭。

什么事IO流

上圖將 ByteArrayInputStream 封裝成  DataInputStream,可以將輸入的字節(jié)數(shù)組轉(zhuǎn)換為對(duì)應(yīng)數(shù)據(jù)類型的數(shù)據(jù)。例如希望讀入int類型數(shù)據(jù),就會(huì)以2個(gè)字節(jié)為單位轉(zhuǎn)換為一個(gè)數(shù)字。

Java IO 的核心類 File

Java 提供了  File類,它指向計(jì)算機(jī)操作系統(tǒng)中的文件和目錄,通過該類只能訪問文件和目錄,無法訪問內(nèi)容。它內(nèi)部主要提供了 3 種操作:

  • 訪問文件的屬性:絕對(duì)路徑、相對(duì)路徑、文件名······

  • 文件檢測:是否文件、是否目錄、文件是否存在、文件的讀/寫/執(zhí)行權(quán)限······

  • 操作文件:創(chuàng)建目錄、創(chuàng)建文件、刪除文件······

上面舉例的操作都是在開發(fā)中非常常用的,F(xiàn)ile 類遠(yuǎn)不止這些操作,更多的操作可以直接去 API 文檔中根據(jù)需求查找。

訪問文件的屬性:

API功能
String getAbsolutePath()返回該文件處于系統(tǒng)中的絕對(duì)路徑名
String getPath()返回該文件的相對(duì)路徑,通常與 new File() 傳入的路徑相同
String getName()返回該文件的文件名

文件檢測:

API功能
boolean isFIle()校驗(yàn)該路徑指向是否一個(gè)文件
boolean isDirectory()校驗(yàn)該路徑指向是否一個(gè)目錄
boolean isExist()校驗(yàn)該路徑指向的文件/目錄是否存在
boolean canWrite()校驗(yàn)該文件是否可寫
boolean canRead()校驗(yàn)該文件是否可讀
boolean canExecute()校驗(yàn)該文件/目錄是否可以被執(zhí)行

操作文件:

API功能
mkdirs()遞歸創(chuàng)建多個(gè)文件夾,路徑中間有可能某些文件夾不存在
createNewFile()創(chuàng)建新文件,它是一個(gè)原子操作,有兩步:檢查文件是否存在、創(chuàng)建新文件
delete()刪除文件或目錄,刪除目錄時(shí)必須保證該目錄為空

多了解一些

文件的讀/寫/執(zhí)行權(quán)限,在 Windows 中通常表現(xiàn)不出來,而在 Linux 中可以很好地體現(xiàn)這一點(diǎn),原因是 Linux  有嚴(yán)格的用戶權(quán)限分組,不同分組下的用戶對(duì)文件有不同的操作權(quán)限,所以這些方法在 Linux 下會(huì)比在 Windows 下更好理解。下圖是 redis  文件夾中的一些文件的詳細(xì)信息,被紅框標(biāo)注的是不同用戶的執(zhí)行權(quán)限:

  • r(Read):代表該文件可以被當(dāng)前用戶讀,操作權(quán)限的序號(hào)是 4

  • w(Write):代表該文件可以被當(dāng)前用戶寫,操作權(quán)限的序號(hào)是 2

  • x(Execute):該文件可以被當(dāng)前用戶執(zhí)行,操作權(quán)限的序號(hào)是 1

  • 什么事IO流

root root 分別代表:當(dāng)前文件的所有者,當(dāng)前文件所屬的用戶分組。Linux 下文件的操作權(quán)限分為三種用戶:

  • 文件所有者:擁有的權(quán)限是紅框中的前三個(gè)字母,-代表沒有某個(gè)權(quán)限

  • 文件所在組的所有用戶:擁有的權(quán)限是紅框中的中間三個(gè)字母

  • 其它組的所有用戶:擁有的權(quán)限是紅框中的最后三個(gè)字母

Java IO 流對(duì)象

回顧流的分類有2種:

  • 根據(jù)數(shù)據(jù)流向分為輸入流和輸出流

  • 根據(jù)數(shù)據(jù)類型分為字節(jié)流和字符流

所以,本小節(jié)將以字節(jié)流和字符流作為主要分割點(diǎn),在其內(nèi)部再細(xì)分為輸入流和輸出流進(jìn)行講解。

什么事IO流

字節(jié)流對(duì)象

字節(jié)流對(duì)象大部分輸入流和輸出流都是成雙成對(duì)地出現(xiàn),所以學(xué)習(xí)的時(shí)候可以將輸入流和輸出流一一對(duì)應(yīng)的流對(duì)象關(guān)聯(lián)起來,輸入流和輸出流只是數(shù)據(jù)流向不同,而處理數(shù)據(jù)的方式可以是相同的。

注意不要認(rèn)為用什么流讀入數(shù)據(jù),就需要用對(duì)應(yīng)的流寫出數(shù)據(jù),在 Java  中沒有這么規(guī)定,下圖只是各個(gè)對(duì)象之間的一個(gè)對(duì)應(yīng)關(guān)系,不是兩個(gè)類使用時(shí)必須強(qiáng)制關(guān)聯(lián)使用。

“下面有非常多的類,我會(huì)介紹基類的方法,了解這些方法是非常有必要的,子類的功能基于父類去擴(kuò)展,只有真正了解父類在做什么,學(xué)習(xí)子類的成本就會(huì)下降?!?/p>

什么事IO流

InputStream

InputStream 是字節(jié)輸入流的抽象基類,提供了通用的讀方法,讓子類使用或重寫它們。下面是 InputStream 常用的重要的方法。

重要方法功能
public abstract int read()從輸入流中讀取下一個(gè)字節(jié),讀到尾部時(shí)返回 -1
public int read(byte b[])從輸入流中讀取長度為 b.length 個(gè)字節(jié)放入字節(jié)數(shù)組 b 中
public int read(byte b[], int off, int len)從輸入流中讀取指定范圍的字節(jié)數(shù)據(jù)放入字節(jié)數(shù)組 b 中
public void close()關(guān)閉此輸入流并釋放與該輸入流相關(guān)的所有資源

還有其它一些不太常用的方法,我也列出來了。

其它方法功能
public long skip(long n)跳過接下來的 n 個(gè)字節(jié),返回實(shí)際上跳過的字節(jié)數(shù)
public long available()返回下一次可讀?。ㄌ^)且不會(huì)被方法阻塞的字節(jié)數(shù)的估計(jì)值
public synchronized void mark(int readlimit)標(biāo)記此輸入流的當(dāng)前位置,對(duì) reset() 方法的后續(xù)調(diào)用將會(huì)重新定位在 mark() 標(biāo)記的位置,可以重新讀取相同的字節(jié)
public boolean markSupported()判斷該輸入流是否支持 mark() 和 reset() 方法,即能否重復(fù)讀取字節(jié)
public synchronized void reset()將流的位置重新定位在最后一次調(diào)用 mark() 方法時(shí)的位置

什么事IO流

(1)ByteArrayInputStream

ByteArrayInputStream 內(nèi)部包含一個(gè) buf 字節(jié)數(shù)組緩沖區(qū),該緩沖區(qū)可以從流中讀取的字節(jié)數(shù),使用 pos  指針指向讀取下一個(gè)字節(jié)的下標(biāo)位置,內(nèi)部還維護(hù)了一個(gè)count 屬性,代表能夠讀取 count 個(gè)字節(jié)。

什么事IO流

bytearrayinputstream

“必須保證 pos 嚴(yán)格小于 count,而 count 嚴(yán)格小于 buf.length 時(shí),才能夠從緩沖區(qū)中讀取數(shù)據(jù)”

(2)FileInputStream

文件輸入流,從文件中讀入字節(jié),通常對(duì)文件的拷貝、移動(dòng)等操作,可以使用該輸入流把文件的字節(jié)讀入內(nèi)存中,然后再利用輸出流輸出到指定的位置上。

(3)PipedInputStream

管道輸入流,它與 PipedOutputStream 成對(duì)出現(xiàn),可以實(shí)現(xiàn)多線程中的管道通信。PipedOutputStream 中指定與特定的  PipedInputStream 連接,PipedInputStream 也需要指定特定的 PipedOutputStream 連接,之后輸出流不斷地往輸入流的  buffer 緩沖區(qū)寫數(shù)據(jù),而輸入流可以從緩沖區(qū)中讀取數(shù)據(jù)。

(4)ObjectInputStream

對(duì)象輸入流,用于對(duì)象的反序列化,將讀入的字節(jié)數(shù)據(jù)反序列化為一個(gè)對(duì)象,實(shí)現(xiàn)對(duì)象的持久化存儲(chǔ)。

(5)PushBackInputStream

它是 FilterInputStream 的子類,是一個(gè)處理流,它內(nèi)部維護(hù)了一個(gè)緩沖數(shù)組buf。

  • 在讀入字節(jié)的過程中可以將讀取到的字節(jié)數(shù)據(jù)回退給緩沖區(qū)中保存,下次可以再次從緩沖區(qū)中讀出該字節(jié)數(shù)據(jù)。所以PushBackInputStream  允許多次讀取輸入流的字節(jié)數(shù)據(jù),只要將讀到的字節(jié)放回緩沖區(qū)即可。

什么事IO流

什么事IO流

什么事IO流

什么事IO流

什么事IO流

什么事IO流

什么事IO流

需要注意的是如果回推字節(jié)時(shí),如果緩沖區(qū)已滿,會(huì)拋出 IOException 異常。

它的應(yīng)用場景:對(duì)數(shù)據(jù)進(jìn)行分類規(guī)整。

假如一個(gè)文件中存儲(chǔ)了數(shù)字和字母兩種類型的數(shù)據(jù),我們需要將它們交給兩種線程各自去收集自己負(fù)責(zé)的數(shù)據(jù),如果采用傳統(tǒng)的做法,把所有的數(shù)據(jù)全部讀入內(nèi)存中,再將數(shù)據(jù)進(jìn)行分離,面對(duì)大文件的情況下,例如1G、2G,傳統(tǒng)的輸入流在讀入數(shù)組后,由于沒有緩沖區(qū),只能對(duì)數(shù)據(jù)進(jìn)行拋棄,這樣每個(gè)線程都要讀一遍文件。

使用 PushBackInputStream 可以讓一個(gè)專門的線程讀取文件,喚醒不同的線程讀取字符:

  • 第一次讀取緩沖區(qū)的數(shù)據(jù),判斷該數(shù)據(jù)由哪些線程讀取

  • 回退數(shù)據(jù),喚醒對(duì)應(yīng)的線程讀取數(shù)據(jù)

  • 重復(fù)前兩步

  • 關(guān)閉輸入流

到這里,你是否會(huì)想到 AQS 的 Condition 等待隊(duì)列,多個(gè)線程可以在不同的條件上等待被喚醒。

(6)BufferedInputStream

緩沖流,它是一種處理流,對(duì)節(jié)點(diǎn)流進(jìn)行封裝并增強(qiáng),其內(nèi)部擁有一個(gè) buffer  緩沖區(qū),用于緩存所有讀入的字節(jié),當(dāng)緩沖區(qū)滿時(shí),才會(huì)將所有字節(jié)發(fā)送給客戶端讀取,而不是每次都只發(fā)送一部分?jǐn)?shù)據(jù),提高了效率。

(7)DataInputStream

數(shù)據(jù)輸入流,它同樣是一種處理流,對(duì)節(jié)點(diǎn)流進(jìn)行封裝后,能夠在內(nèi)部對(duì)讀入的字節(jié)轉(zhuǎn)換為對(duì)應(yīng)的 Java 基本數(shù)據(jù)類型。

(8)SequenceInputStream

將兩個(gè)或多個(gè)輸入流看作是一個(gè)輸入流依次讀取,該類的存在與否并不影響整個(gè) IO 生態(tài),在程序中也能夠做到這種效果

(9)StringBufferInputStream

將字符串中每個(gè)字符的低 8 位轉(zhuǎn)換為字節(jié)讀入到字節(jié)數(shù)組中,目前已過期

InputStream 總結(jié):

  • InputStream 是所有輸入字節(jié)流的抽象基類

  • ByteArrayInputStream 和 FileInputStream 是兩種基本的節(jié)點(diǎn)流,他們分別從字節(jié)數(shù)組 和 本地文件中讀取數(shù)據(jù)

  • DataInputStream、BufferedInputStream 和 PushBackInputStream  都是處理流,對(duì)基本的節(jié)點(diǎn)流進(jìn)行封裝并增強(qiáng)

  • PipiedInputStream 用于多線程通信,可以與其它線程公用一個(gè)管道,讀取管道中的數(shù)據(jù)。

  • ObjectInputStream 用于對(duì)象的反序列化,將對(duì)象的字節(jié)數(shù)據(jù)讀入內(nèi)存中,通過該流對(duì)象可以將字節(jié)數(shù)據(jù)轉(zhuǎn)換成對(duì)應(yīng)的對(duì)象

OutputStream

OutputStream 是字節(jié)輸出流的抽象基類,提供了通用的寫方法,讓繼承的子類重寫和復(fù)用。

方法功能
public abstract void write(int b)將指定的字節(jié)寫出到輸出流,寫入的字節(jié)是參數(shù) b 的低 8 位
public void write(byte b[])將指定字節(jié)數(shù)組中的所有字節(jié)寫入到輸出流當(dāng)中
public void write(byte b[], int off, int len)指定寫入的起始位置 offer,字節(jié)數(shù)為 len 的字節(jié)數(shù)組寫入到輸出流當(dāng)中
public void flush()刷新此輸出流,并強(qiáng)制寫出所有緩沖的輸出字節(jié)到指定位置,每次寫完都要調(diào)用
public void close()關(guān)閉此輸出流并釋放與此流關(guān)聯(lián)的所有系統(tǒng)資源

什么事IO流


OutputStream 中大多數(shù)的類和 InputStream 是對(duì)應(yīng)的,只不過數(shù)據(jù)的流向不同而已。從上面的圖可以看出:

  • OutputStream 是所有輸出字節(jié)流的抽象基類

  • ByteArrayOutputStream 和 FileOutputStream 是兩種基本的節(jié)點(diǎn)流,它們分別向字節(jié)數(shù)組和本地文件寫出數(shù)據(jù)

  • DataOutputStream、BufferedOutputStream  是處理流,前者可以將字節(jié)數(shù)據(jù)轉(zhuǎn)換成基本數(shù)據(jù)類型寫出到文件中;后者是緩沖字節(jié)數(shù)組,只有在緩沖區(qū)滿時(shí),才會(huì)將所有的字節(jié)寫出到目的地,減少了 IO 次數(shù)。

  • PipedOutputStream 用于多線程通信,可以和其它線程共用一個(gè)管道,向管道中寫入數(shù)據(jù)

  • ObjectOutputStream 用于對(duì)象的序列化,將對(duì)象轉(zhuǎn)換成字節(jié)數(shù)組后,將所有的字節(jié)都寫入到指定位置中

  • PrintStream 在 OutputStream 基礎(chǔ)之上提供了增強(qiáng)的功能,即可以方便地輸出各種類型的數(shù)據(jù)(而不僅限于byte型)的格式化表示形式,且  PrintStream 的方法從不拋出 IOEception,其原理是寫出時(shí)將各個(gè)數(shù)據(jù)類型的數(shù)據(jù)統(tǒng)一轉(zhuǎn)換為 String 類型,我會(huì)在講解完

字符流對(duì)象

字符流對(duì)象也會(huì)有對(duì)應(yīng)關(guān)系,大多數(shù)的類可以認(rèn)為是操作的數(shù)據(jù)從字節(jié)數(shù)組變?yōu)樽址?,類的功能和字?jié)流對(duì)象是相似的。

“字符輸入流和字節(jié)輸入流的組成非常相似,字符輸入流是對(duì)字節(jié)輸入流的一層轉(zhuǎn)換,所有文件的存儲(chǔ)都是字節(jié)的存儲(chǔ),在磁盤上保留的不是文件的字符,而是先把字符編碼成字節(jié),再保存到文件中。在讀取文件時(shí),讀入的也是一個(gè)一個(gè)字節(jié)組成的字節(jié)序列,而  Java 虛擬機(jī)通過將字節(jié)序列,按照2個(gè)字節(jié)為單位轉(zhuǎn)換為 Unicode 字符,實(shí)現(xiàn)字節(jié)到字符的映射。”

什么事IO流

Reader

Reader 是字符輸入流的抽象基類,它內(nèi)部的重要方法如下所示。

重要方法方法功能
public int read(java.nio.CharBuffer target)將讀入的字符存入指定的字符緩沖區(qū)中
public int read()讀取一個(gè)字符
public int read(char cbuf[])讀入字符放入整個(gè)字符數(shù)組中
abstract public int read(char cbuf[], int off, int len)將字符讀入字符數(shù)組中的指定范圍中

還有其它一些額外的方法,與字節(jié)輸入流基類提供的方法是相同的,只是作用的對(duì)象不再是字節(jié),而是字符。

什么事IO流

  • Reader 是所有字符輸入流的抽象基類

  • CharArrayReader 和 StringReader 是兩種基本的節(jié)點(diǎn)流,它們分別從讀取 字符數(shù)組和 字符串 數(shù)據(jù),StringReader  內(nèi)部是一個(gè) String 變量值,通過遍歷該變量的字符,實(shí)現(xiàn)讀取字符串,本質(zhì)上也是在讀取字符數(shù)組

  • PipedReader 用于多線程中的通信,從共用地管道中讀取字符數(shù)據(jù)

  • BufferedReader 是字符輸入緩沖流,將讀入的數(shù)據(jù)放入字符緩沖區(qū)中,實(shí)現(xiàn)高效地讀取字符

  • InputStreamReader 是一種轉(zhuǎn)換流,可以實(shí)現(xiàn)從字節(jié)流轉(zhuǎn)換為字符流,將字節(jié)數(shù)據(jù)轉(zhuǎn)換為字符

Writer

Reader 是字符輸出流的抽象基類,它內(nèi)部的重要方法如下所示。

重要方法方法功能
public void write(char cbuf[])將 cbuf 字符數(shù)組寫出到輸出流
abstract public void write(char cbuf[], int off, int len)將指定范圍的 cbuf 字符數(shù)組寫出到輸出流
public void write(String str)將字符串 str 寫出到輸出流,str 內(nèi)部也是字符數(shù)組
public void write(String str, int off, int len)將字符串 str 的某一部分寫出到輸出流
abstract public void flush()刷新,如果數(shù)據(jù)保存在緩沖區(qū),調(diào)用該方法才會(huì)真正寫出到指定位置
abstract public void close()關(guān)閉流對(duì)象,每次 IO 執(zhí)行完畢后都需要關(guān)閉流對(duì)象,釋放系統(tǒng)資源

什么事IO流

  • Writer 是所有的輸出字符流的抽象基類

  • CharArrayWriter、StringWriter 是兩種基本的節(jié)點(diǎn)流,它們分別向Char 數(shù)組、字符串中寫入數(shù)據(jù)。StringWriter  內(nèi)部保存了 StringBuffer 對(duì)象,可以實(shí)現(xiàn)字符串的動(dòng)態(tài)增長

  • PipedWriter 可以向共用的管道中寫入字符數(shù)據(jù),給其它線程讀取。

  • BufferedWriter 是緩沖輸出流,可以將寫出的數(shù)據(jù)緩存起來,緩沖區(qū)滿時(shí)再調(diào)用 flush() 寫出數(shù)據(jù),減少 IO 次數(shù)。

  • PrintWriter 和 PrintStream 類似,功能和使用也非常相似,只是寫出的數(shù)據(jù)是字符而不是字節(jié)。

  • OutputStreamWriter 將字符流轉(zhuǎn)換為字節(jié)流,將字符寫出到指定位置

字節(jié)流與字符流的轉(zhuǎn)換

從任何地方把數(shù)據(jù)讀入到內(nèi)存都是先以字節(jié)流形式讀取,即使是使用字符流去讀取數(shù)據(jù),依然成立,因?yàn)閿?shù)據(jù)永遠(yuǎn)是以字節(jié)的形式存在于互聯(lián)網(wǎng)和硬件設(shè)備中,字符流是通過字符集的映射,才能夠?qū)⒆止?jié)轉(zhuǎn)換為字符。

所以 Java 提供了兩種轉(zhuǎn)換流:

  • InputStreamReader:從字節(jié)流轉(zhuǎn)換為字符流,將字節(jié)數(shù)據(jù)轉(zhuǎn)換為字符數(shù)據(jù)讀入到內(nèi)存

  • OutputStreamWriter:從字符流轉(zhuǎn)換為字節(jié)流,將字符數(shù)據(jù)轉(zhuǎn)換為字節(jié)數(shù)據(jù)寫出到指定位置

了解了 Java 傳統(tǒng)的 BIO 中字符流和字節(jié)流的主要成員之后,至少要掌握以下兩個(gè)關(guān)鍵點(diǎn):

(1)傳統(tǒng)的 BIO 是以流為基本單位處理數(shù)據(jù)的,想象成水流,一點(diǎn)點(diǎn)地傳輸字節(jié)數(shù)據(jù),IO 流傳輸?shù)倪^程永遠(yuǎn)是以字節(jié)形式傳輸。

(2)字節(jié)流和字符流的區(qū)別在于操作的數(shù)據(jù)單位不相同,字符流是通過將字節(jié)數(shù)據(jù)通過字符集映射成對(duì)應(yīng)的字符,字符流本質(zhì)上也是字節(jié)流。

接下來我們再繼續(xù)學(xué)習(xí) NIO 知識(shí),NIO 是當(dāng)下非?;馃岬囊环N IO 工作方式,它能夠解決傳統(tǒng) BIO 的痛點(diǎn):阻塞。

  • BIO 如果遇到 IO 阻塞時(shí),線程將會(huì)被掛起,直到 IO 完成后才喚醒線程,線程切換帶來了額外的開銷。

  • BIO 中每個(gè) IO 都需要有對(duì)應(yīng)的一個(gè)線程去專門處理該次 IO 請求,會(huì)讓服務(wù)器的壓力迅速提高。

我們希望做到的是當(dāng)線程等待 IO 完成時(shí)能夠去完成其它事情,當(dāng) IO 完成時(shí)線程可以回來繼續(xù)處理 IO 相關(guān)操作,不必干干的坐等 IO 完成。在 IO  處理的過程中,能夠有一個(gè)專門的線程負(fù)責(zé)監(jiān)聽這些 IO 操作,通知服務(wù)器該如何操作。所以,我們聊到 IO,不得不去接觸 NIO 這一塊硬骨頭。

新潮的 NIO

我們來看看 BIO 和 NIO 的區(qū)別,BIO 是面向流的  IO,它建立的通道都是單向的,所以輸入和輸出流的通道不相同,必須建立2個(gè)通道,通道內(nèi)的都是傳輸==0101001···==的字節(jié)數(shù)據(jù)。

什么事IO流

而在 NIO 中,不再是面向流的 IO  了,而是面向緩沖區(qū),它會(huì)建立一個(gè)通道(Channel),該通道我們可以理解為鐵路,該鐵路上可以運(yùn)輸各種貨物,而通道上會(huì)有一個(gè)緩沖區(qū)(Buffer)用于存儲(chǔ)真正的數(shù)據(jù),緩沖區(qū)我們可以理解為一輛火車。

通道(鐵路)只是作為運(yùn)輸數(shù)據(jù)的一個(gè)連接資源,而真正存儲(chǔ)數(shù)據(jù)的是緩沖區(qū)(火車)。即通道負(fù)責(zé)傳輸,緩沖區(qū)負(fù)責(zé)存儲(chǔ)。

什么事IO流

理解了上面的圖之后,BIO 和 NIO 的主要區(qū)別就可以用下面這個(gè)表格簡單概括。

BIONIO
面向流(Stream)面向緩沖區(qū)(Buffer)
單向通道雙向通道
阻塞 IO非阻塞 IO
 選擇器(Selectors)

緩沖區(qū)(Buffer)

緩沖區(qū)是存儲(chǔ)數(shù)據(jù)的區(qū)域,在 Java 中,緩沖區(qū)就是數(shù)組,為了可以操作不同數(shù)據(jù)類型的數(shù)據(jù),Java  提供了許多不同類型的緩沖區(qū),除了布爾類型以外,其它基本數(shù)據(jù)類型都有對(duì)應(yīng)的緩沖區(qū)數(shù)組對(duì)象。

什么事IO流

“為什么沒有布爾類型的緩沖區(qū)呢?

在 Java 中,boolean 類型數(shù)據(jù)只占用 1 bit,而在 IO 傳輸過程中,都是以字節(jié)為單位進(jìn)行傳輸?shù)?,所?boolean 的 1 bit  完全可以使用 byte 類型的某一位,或者 int 類型的某一位來表示,沒有必要為了這 1 bit 而專門提供多一個(gè)緩沖區(qū)?!?/p>

緩沖區(qū)解釋
ByteBuffer存儲(chǔ)字節(jié)數(shù)據(jù)的緩沖區(qū)
CharBuffer存儲(chǔ)字符數(shù)據(jù)的緩沖區(qū)
ShortBuffer存儲(chǔ)短整型數(shù)據(jù)的緩沖區(qū)
IntBuffer存儲(chǔ)整型數(shù)據(jù)的緩沖區(qū)
LongBuffer存儲(chǔ)長整型數(shù)據(jù)的緩沖區(qū)
FloatBuffer存儲(chǔ)單精度浮點(diǎn)型數(shù)據(jù)的緩沖區(qū)
DoubleBuffer存儲(chǔ)雙精度浮點(diǎn)型數(shù)據(jù)的緩沖區(qū)

分配一個(gè)緩沖區(qū)的方式都高度一致:使用allocate(int capacity)方法。

例如需要分配一個(gè) 1024 大小的字節(jié)數(shù)組,代碼就是下面這樣子。

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

緩沖區(qū)讀寫數(shù)據(jù)的兩個(gè)核心方法:

  • put():將數(shù)據(jù)寫入到緩沖區(qū)中

  • get():從緩沖區(qū)中讀取數(shù)據(jù)

緩沖區(qū)的重要屬性:

  • capacity:緩沖區(qū)中最大存儲(chǔ)數(shù)據(jù)的容量,一旦聲明則無法改變

  • limit:表示緩沖區(qū)中可以操作數(shù)據(jù)的大小,limit 之后的數(shù)據(jù)無法進(jìn)行讀寫。必須滿足 limit <= capacity

  • position:當(dāng)前緩沖區(qū)中正在操作數(shù)據(jù)的下標(biāo)位置,必須滿足 position <= limit

  • mark:標(biāo)記位置,調(diào)用 reset() 將 position 位置調(diào)整到 mark 屬性指向的下標(biāo)位置,實(shí)現(xiàn)多次讀取數(shù)據(jù)

緩沖區(qū)為高效讀寫數(shù)據(jù)而提供的其它輔助方法:

  • flip():可以實(shí)現(xiàn)讀寫模式的切換,我們可以看看里面的源碼

public final Buffer flip() {     limit = position;     position = 0;     mark = -1;     return this; }

調(diào)用 flip() 會(huì)將可操作的大小 limit 設(shè)置為當(dāng)前寫的位置,操作數(shù)據(jù)的起始位置 position 設(shè)置為 0,即從頭開始讀取數(shù)據(jù)。

  • rewind():可以將 position 位置設(shè)置為 0,再次讀取緩沖區(qū)中的數(shù)據(jù)

  • clear():清空整個(gè)緩沖區(qū),它會(huì)將 position 設(shè)置為 0,limit 設(shè)置為 capacity,可以寫整個(gè)緩沖區(qū)

“更多的方法可以去查閱 API 文檔,本文礙于篇幅原因就不貼出其它方法了,主要是要理解緩沖區(qū)的作用”

我們來看一個(gè)簡單的例子

public Class Main {     public static void main(String[] args) {          // 分配內(nèi)存大小為11的整型緩存區(qū)         IntBuffer buffer = IntBuffer.allocate(11);         // 往buffer里寫入2個(gè)整型數(shù)據(jù)         for (int i = 0; i < 2; ++i) {             int randomNum = new SecureRandom().nextInt();             buffer.put(randomNum);         }         // 將Buffer從寫模式切換到讀模式         buffer.flip();         System.out.println("position >> " + buffer.position()                            + "limit >> " + buffer.limit()                             + "capacity >> " + buffer.capacity());         // 讀取buffer里的數(shù)據(jù)         while (buffer.hasRemaining()) {             System.out.println(buffer.get());         }         System.out.println("position >> " + buffer.position()                            + "limit >> " + buffer.limit()                             + "capacity >> " + buffer.capacity());     } }

執(zhí)行結(jié)果如下圖所示,首先我們往緩沖區(qū)中寫入 2 個(gè)數(shù)據(jù),position 在寫模式下指向下標(biāo) 2,然后調(diào)用 flip() 方法切換為讀模式,limit  指向下標(biāo) 2,position 從 0 開始讀數(shù)據(jù),讀到下標(biāo)為 2 時(shí)發(fā)現(xiàn)到達(dá) limit 位置,不可繼續(xù)讀。

什么事IO流

整個(gè)過程可以用下圖來理解,調(diào)用 flip() 方法以后,讀出數(shù)據(jù)的同時(shí) position 指針不斷往后挪動(dòng),到達(dá) limit  指針的位置時(shí),該次讀取操作結(jié)束。

什么事IO流

“介紹完緩沖區(qū)后,我們知道它是存儲(chǔ)數(shù)據(jù)的空間,進(jìn)程可以將緩沖區(qū)中的數(shù)據(jù)讀取出來,也可以寫入新的數(shù)據(jù)到緩沖區(qū),那緩沖區(qū)的數(shù)據(jù)從哪里來,又怎么寫出去呢?接下來我們需要學(xué)習(xí)傳輸數(shù)據(jù)的介質(zhì):通道(Channel)”

通道(Channel)

上面我們介紹過,通道是作為一種連接資源,作用是傳輸數(shù)據(jù),而真正存儲(chǔ)數(shù)據(jù)的是緩沖區(qū),所以介紹完緩沖區(qū)后,我們來學(xué)習(xí)通道這一塊。

通道是可以雙向讀寫的,傳統(tǒng)的 BIO 需要使用輸入/輸出流表示數(shù)據(jù)的流向,在 NIO 中可以減少通道資源的消耗。

什么事IO流

通道類都保存在 java.nio.channels 包下,我們?nèi)粘S玫降膸讉€(gè)重要的類有 4 個(gè):

IO 通道類型具體類
文件 IOFileChannel(用于文件讀寫、操作文件的通道)
TCP 網(wǎng)絡(luò) IOSocketChannel(用于讀寫數(shù)據(jù)的 TCP 通道)、ServerSocketChannel(監(jiān)聽客戶端的連接)
UDP 網(wǎng)絡(luò) IODatagramChannel(收發(fā) UDP 數(shù)據(jù)報(bào)的通道)

可以通過 getChannel() 方法獲取一個(gè)通道,支持獲取通道的類如下:

  • 文件 IO:FileInputStream、FileOutputStream、RandomAccessFile

  • TCP 網(wǎng)絡(luò) IO:Socket、ServerSocket

  • UDP 網(wǎng)絡(luò) IO:DatagramSocket

示例:文件拷貝案例

我們來看一個(gè)利用通道拷貝文件的例子,需要下面幾個(gè)步驟:

  • 打開原文件的輸入流通道,將字節(jié)數(shù)據(jù)讀入到緩沖區(qū)中

  • 打開目的文件的輸出流通道,將緩沖區(qū)中的數(shù)據(jù)寫到目的地

  • 關(guān)閉所有流和通道(重要!)

這是一張小菠蘿的照片,它存在于d:\小菠蘿\文件夾下,我們將它拷貝到 d:\小菠蘿分身\文件夾下。

什么事IO流

public class Test {  /** 緩沖區(qū)的大小 */     public static final int SIZE = 1024;      public static void main(String[] args) throws IOException {         // 打開文件輸入流         FileChannel inChannel = new FileInputStream("d:\小菠蘿\小菠蘿.jpg").getChannel();         // 打開文件輸出流         FileChannel outChannel = new FileOutputStream("d:\小菠蘿分身\小菠蘿-拷貝.jpg").getChannel();         // 分配 1024 個(gè)字節(jié)大小的緩沖區(qū)         ByteBuffer dsts = ByteBuffer.allocate(SIZE);         // 將數(shù)據(jù)從通道讀入緩沖區(qū)         while (inChannel.read(dsts) != -1) {             // 切換緩沖區(qū)的讀寫模式             dsts.flip();             // 將緩沖區(qū)的數(shù)據(jù)通過通道寫到目的地             outChannel.write(dsts);             // 清空緩沖區(qū),準(zhǔn)備下一次讀             dsts.clear();         }         inChannel.close();         outChannel.close();     }  }

我畫了一張圖幫助你理解上面的這一個(gè)過程。

什么事IO流

“有人會(huì)問,NIO 的文件拷貝和傳統(tǒng) IO 流的文件拷貝有何不同呢?我們在編程時(shí)感覺它們沒有什么區(qū)別呀,貌似只是 API  不同罷了,我們接下來就去看看這兩者之間的區(qū)別吧?!?/p>

BIO 和 NIO 拷貝文件的區(qū)別

這個(gè)時(shí)候就要來了解了解操作系統(tǒng)底層是怎么對(duì) IO 和 NIO 進(jìn)行區(qū)別的,我會(huì)用盡量通俗的文字帶你理解,可能并不是那么嚴(yán)謹(jǐn)。

操作系統(tǒng)最重要的就是內(nèi)核,它既可以訪問受保護(hù)的內(nèi)存,也可以訪問底層硬件設(shè)備,所以為了保護(hù)內(nèi)核的安全,操作系統(tǒng)將底層的虛擬空間分為了用戶空間和內(nèi)核空間,其中用戶空間就是給用戶進(jìn)程使用的,內(nèi)核空間就是專門給操作系統(tǒng)底層去使用的。

什么事IO流

接下來,有一個(gè) Java 進(jìn)程希望把小菠蘿這張圖片從磁盤上拷貝,那么內(nèi)核空間和用戶空間都會(huì)有一個(gè)緩沖區(qū)

  • 這張照片就會(huì)從磁盤中讀出到內(nèi)核緩沖區(qū)中保存,然后操作系統(tǒng)將內(nèi)核緩沖區(qū)中的這張圖片字節(jié)數(shù)據(jù)拷貝到用戶進(jìn)程的緩沖區(qū)中保存下來,對(duì)應(yīng)著下面這幅圖

什么事IO流

  • 然后用戶進(jìn)程會(huì)希望把緩沖區(qū)中的字節(jié)數(shù)據(jù)寫到磁盤上的另外一個(gè)地方,會(huì)將數(shù)據(jù)拷貝到 Socket 緩沖區(qū)中,最終操作系統(tǒng)再將 Socket  緩沖區(qū)的數(shù)據(jù)寫到磁盤的指定位置上。

什么事IO流

這一輪操作下來,我們數(shù)數(shù)經(jīng)過了幾次數(shù)據(jù)的拷貝?4 次。有 2  次是內(nèi)核空間和用戶空間之間的數(shù)據(jù)拷貝,這兩次拷貝涉及到用戶態(tài)和內(nèi)核態(tài)的切換,需要CPU參與進(jìn)來,進(jìn)行上下文切換。而另外 2  次是硬盤和內(nèi)核空間之間的數(shù)據(jù)拷貝,這個(gè)過程利用到 DMA與系統(tǒng)內(nèi)存交換數(shù)據(jù),不需要 CPU 的參與。

導(dǎo)致 IO 性能瓶頸的原因:內(nèi)核空間與用戶空間之間數(shù)據(jù)過多無意義的拷貝,以及多次上下文切換

操作狀態(tài)
用戶進(jìn)程請求讀取數(shù)據(jù)用戶態(tài) -> 內(nèi)核態(tài)
操作系統(tǒng)內(nèi)核返回?cái)?shù)據(jù)給用戶進(jìn)程內(nèi)核態(tài) -> 用戶態(tài)
用戶進(jìn)程請求寫數(shù)據(jù)到硬盤用戶態(tài) -> 內(nèi)核態(tài)
操作系統(tǒng)返回操作結(jié)果給用戶進(jìn)程內(nèi)核態(tài) -> 用戶態(tài)

“在用戶空間與內(nèi)核空間之間的操作,會(huì)涉及到上下文的切換,這里需要 CPU 的干預(yù),而數(shù)據(jù)在兩個(gè)空間之間來回拷貝,也需要 CPU 的干預(yù),這無疑會(huì)增大 CPU  的壓力,NIO 是如何減輕 CPU 的壓力?運(yùn)用操作系統(tǒng)的零拷貝技術(shù)?!?/p>

操作系統(tǒng)的零拷貝

所以,操作系統(tǒng)出現(xiàn)了一個(gè)全新的概念,解決了 IO 瓶頸:零拷貝。零拷貝指的是內(nèi)核空間與用戶空間之間的零次拷貝。

零拷貝可以說是 IO 的一大救星,操作系統(tǒng)底層有許多種零拷貝機(jī)制,我這里僅針對(duì) Java NIO 中使用到的其中一種零拷貝機(jī)制展開講解。

在 Java NIO 中,零拷貝是通過用戶空間和內(nèi)核空間的緩沖區(qū)共享一塊物理內(nèi)存實(shí)現(xiàn)的,也就是說上面的圖可以演變成這個(gè)樣子。

什么事IO流

這時(shí),無論是用戶空間還是內(nèi)核空間操作自己的緩沖區(qū),本質(zhì)上都是操作這一塊共享內(nèi)存中的緩沖區(qū)數(shù)據(jù),省去了用戶空間和內(nèi)核空間之間的數(shù)據(jù)拷貝操作。

現(xiàn)在我們重新來拷貝文件,就會(huì)變成下面這個(gè)步驟:

  • 用戶進(jìn)程通過系統(tǒng)調(diào)用 read() 請求讀取文件到用戶空間緩沖區(qū)(第一次上下文切換),用戶態(tài) ->  核心態(tài),數(shù)據(jù)從硬盤讀取到內(nèi)核空間緩沖區(qū)中(第一次數(shù)據(jù)拷貝)

  • 系統(tǒng)調(diào)用返回到用戶進(jìn)程(第二次上下文切換),此時(shí)用戶空間與內(nèi)核空間共享這一塊內(nèi)存(緩沖區(qū)),所以不需要從內(nèi)核緩沖區(qū)拷貝到用戶緩沖區(qū)

  • 用戶進(jìn)程發(fā)出 write() 系統(tǒng)調(diào)用請求寫數(shù)據(jù)到硬盤上(第三次上下文切換),此時(shí)需要將內(nèi)核空間緩沖區(qū)中的數(shù)據(jù)拷貝到內(nèi)核的 Socket  緩沖區(qū)中(第二次數(shù)據(jù)拷貝)

  • 由 DMA 將 Socket 緩沖區(qū)的內(nèi)容寫到硬盤上(第三次數(shù)據(jù)拷貝),write() 系統(tǒng)調(diào)用返回(第四次上下文切換)

整個(gè)過程就如下面這幅圖所示。

什么事IO流

圖中,需要 CPU 參與工作的步驟只有第③個(gè)步驟,對(duì)比于傳統(tǒng)的 IO,CPU 需要在用戶空間與內(nèi)核空間之間參與拷貝工作,需要無意義地占用 2 次 CPU  資源,導(dǎo)致 CPU 資源的浪費(fèi)。

下面總結(jié)一下操作系統(tǒng)中零拷貝的優(yōu)點(diǎn):

  • 降低 CPU 的壓力:避免 CPU 需要參與內(nèi)核空間與用戶空間之間的數(shù)據(jù)拷貝工作

  • 減少不必要的拷貝:避免用戶空間與內(nèi)核空間之間需要進(jìn)行數(shù)據(jù)拷貝

上面的圖示可能并不嚴(yán)謹(jǐn),對(duì)于你理解零拷貝會(huì)有一定的幫助,關(guān)于零拷貝的知識(shí)點(diǎn)可以去查閱更多資料哦,這是一門大學(xué)問。

“介紹完通道后,我們知道它是用于傳輸數(shù)據(jù)的一種介質(zhì),而且是可以雙向讀寫的,那么如果放在網(wǎng)絡(luò) IO  中,這些通道如果有數(shù)據(jù)就緒時(shí),服務(wù)器是如何發(fā)現(xiàn)并處理的呢?接下來我們?nèi)W(xué)習(xí) NIO 中的最后一個(gè)重要知識(shí)點(diǎn):選擇器(Selector)”

選擇器(Selectors)

選擇器是提升 IO 性能的靈魂之一,它底層利用了多路復(fù)用 IO機(jī)制,讓選擇器可以監(jiān)聽多個(gè) IO 連接,根據(jù) IO  的狀態(tài)響應(yīng)到服務(wù)器端進(jìn)行處理。通俗地說:選擇器可以監(jiān)聽多個(gè) IO 連接,而傳統(tǒng)的 BIO 每個(gè) IO 連接都需要有一個(gè)線程去監(jiān)聽和處理。

什么事IO流

圖中很明顯的顯示了在 BIO 中,每個(gè) Socket 都需要有一個(gè)專門的線程去處理每個(gè)請求,而在 NIO 中,只需要一個(gè) Selector 即可監(jiān)聽各個(gè)  Socket 請求,而且 Selector 并不是阻塞的,所以不會(huì)因?yàn)槎鄠€(gè)線程之間切換導(dǎo)致上下文切換帶來的開銷。

什么事IO流

在 Java NIO 中,選擇器是使用 Selector 類表示,Selector 可以接收各種 IO 連接,在 IO 狀態(tài)準(zhǔn)備就緒時(shí),會(huì)通知該通道注冊的  Selector,Selector 在下一次輪詢時(shí)會(huì)發(fā)現(xiàn)該 IO 連接就緒,進(jìn)而處理該連接。

Selector 選擇器主要用于網(wǎng)絡(luò) IO當(dāng)中,在這里我會(huì)將傳統(tǒng)的 BIO Socket 編程和使用 NIO 后的 Socket 編程作對(duì)比,分析 NIO  為何更受歡迎。首先先來了解 Selector 的基本結(jié)構(gòu)。

重要方法方法解析
open()打開一個(gè) Selector 選擇器
int select()阻塞地等待就緒的通道
int select(long timeout)最多阻塞 timeout 毫秒,如果是 0 則一直阻塞等待,如果是 1 則代表最多阻塞 1 毫秒
int selectNow()非阻塞地輪詢就緒的通道

在這里,你會(huì)看到 select() 和它的重載方法是會(huì)阻塞的,如果用戶進(jìn)程輪詢時(shí)發(fā)現(xiàn)沒有就緒的通道,操作系統(tǒng)有兩種做法:

  • 一直等待直到一個(gè)就緒的通道,再返回給用戶進(jìn)程

  • 立即返回一個(gè)錯(cuò)誤狀態(tài)碼給用戶進(jìn)程,讓用戶進(jìn)程繼續(xù)運(yùn)行,不會(huì)阻塞

這兩種方法對(duì)應(yīng)了同步阻塞 IO 和 同步非阻塞 IO ,這里讀者的一點(diǎn)小的觀點(diǎn),請各位大神批判閱讀

“Java 中的 NIO 不能真正意義上稱為 Non-Blocking IO,我們通過 API 的調(diào)用可以發(fā)現(xiàn),select()  方法還是會(huì)存在阻塞的現(xiàn)象,根據(jù)傳入的參數(shù)不同,操作系統(tǒng)的行為也會(huì)有所不同,不同之處就是阻塞還是非阻塞,所以我更傾向于把 NIO 稱為 New  IO,因?yàn)樗粌H提供了 Non-Blocking IO,而且保留原有的 Blocking IO 的功能。”

了解了選擇器之后,它的作用就是:監(jiān)聽多個(gè) IO 通道,當(dāng)有通道就緒時(shí)選擇器會(huì)輪詢發(fā)現(xiàn)該通道,并做相應(yīng)的處理。那么 IO  狀態(tài)分為很多種,我們?nèi)绾稳プR(shí)別就緒的通道是處于哪種狀態(tài)呢?在 Java 中提供了選擇鍵(SelectionKey)。

選擇鍵(SelectionKey)

在 Java 中提供了 4 種選擇鍵:

  • SelectionKey.OP_READ:套接字通道準(zhǔn)備好進(jìn)行讀操作

  • SelectionKey.OP_WRITE:套接字通道準(zhǔn)備好進(jìn)行寫操作

  • SelectionKey.OP_ACCEPT:服務(wù)器套接字通道接受其它通道

  • SelectionKey.OP_CONNECT:套接字通道準(zhǔn)備完成連接

在 SelectionKey 中包含了許多屬性

  • channel:該選擇鍵綁定的通道

  • selector:輪詢到該選擇鍵的選擇器

  • readyOps:當(dāng)前就緒選擇鍵的值

  • interesOps:該選擇器對(duì)該通道感興趣的所有選擇鍵

選擇鍵的作用是:在選擇器輪詢到有就緒通道時(shí),會(huì)返回這些通道的就緒選擇鍵(SelectionKey),通過選擇鍵可以獲取到通道進(jìn)行操作。

簡單了解了選擇器后,我們可以結(jié)合緩沖區(qū)、通道和選擇器來完成一個(gè)簡易的聊天室應(yīng)用。

示例:簡易的客戶端服務(wù)器通信

“先說明,這里的代碼非常的臭和長,不推薦細(xì)看,直接看注釋附近的代碼即可?!?/p>

我們在服務(wù)器端會(huì)開辟兩個(gè)線程

  • Thread1:專門監(jiān)聽客戶端的連接,并把通道注冊到客戶端選擇器上

  • Thread2:專門監(jiān)聽客戶端的其它 IO 狀態(tài)(讀狀態(tài)),當(dāng)客戶端的 IO 狀態(tài)就緒時(shí),該選擇器會(huì)輪詢發(fā)現(xiàn),并作相應(yīng)處理

public class NIOServer {          Selector serverSelector = Selector.open();     Selector clientSelector = Selector.open();          public static void main(String[] args) throws IOException {         NIOServer server = nwe NIOServer();         new Thread(() -> {             try {                 // 對(duì)應(yīng)IO編程中服務(wù)端啟動(dòng)                 ServerSocketChannel listenerChannel = ServerSocketChannel.open();                 listenerChannel.socket().bind(new InetSocketAddress(3333));                 listenerChannel.configureBlocking(false);                 listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);     server.acceptListener();             } catch (IOException ignored) {             }         }).start();         new Thread(() -> {             try {                 server.clientListener();             } catch (IOException ignored) {             }         }).start();     } } // 監(jiān)聽客戶端連接 public void acceptListener() {     while (true) {         if (serverSelector.select(1) > 0) {             Set<SelectionKey> set = serverSelector.selectedKeys();             Iterator<SelectionKey> keyIterator = set.iterator();             while (keyIterator.hasNext()) {                 SelectionKey key = keyIterator.next();                 if (key.isAcceptable()) {                     try {                         // (1) 每來一個(gè)新連接,注冊到clientSelector                         SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();                         clientChannel.configureBlocking(false);                         clientChannel.register(clientSelector, SelectionKey.OP_READ);                     } finally {                         // 從就緒的列表中移除這個(gè)key                         keyIterator.remove();                     }                 }             }         }     } } // 監(jiān)聽客戶端的 IO 狀態(tài)就緒 public void clientListener() {     while (true) {         // 批量輪詢是否有哪些連接有數(shù)據(jù)可讀         if (clientSelector.select(1) > 0) {             Set<SelectionKey> set = clientSelector.selectedKeys();             Iterator<SelectionKey> keyIterator = set.iterator();             while (keyIterator.hasNext()) {                 SelectionKey key = keyIterator.next();     // 判斷該通道是否讀就緒狀態(tài)                 if (key.isReadable()) {                     try {                         // 獲取客戶端通道讀入數(shù)據(jù)                         SocketChannel clientChannel = (SocketChannel) key.channel();                         ByteBuffer byteBuffer = ByteBuffer.allocate(1024);                         clientChannel.read(byteBuffer);                         byteBuffer.flip();                         System.out.println(                             LocalDateTime.now().toString() + " Server 端接收到來自 Client 端的消息: " +                             Charset.defaultCharset().decode(byteBuffer).toString());                     } finally {                         // 從就緒的列表中移除這個(gè)key                         keyIterator.remove();                         key.interestOps(SelectionKey.OP_READ);                     }                 }             }         }     } }

在客戶端,我們可以簡單的輸入一些文字,發(fā)送給服務(wù)器

public class NIOClient {          public static final int CAPACITY = 1024;      public static void main(String[] args) throws Exception {         ByteBuffer dsts = ByteBuffer.allocate(CAPACITY);         SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 3333));         socketChannel.configureBlocking(false);         Scanner sc = new Scanner(System.in);         while (true) {             String msg = sc.next();             dsts.put(msg.getBytes());             dsts.flip();             socketChannel.write(dsts);             dsts.clear();         }     }      }

下圖可以看見,在客戶端給服務(wù)器端發(fā)送信息,服務(wù)器接收到消息后,可以將該條消息分發(fā)給其它客戶端,就可以實(shí)現(xiàn)一個(gè)簡單的群聊系統(tǒng),我們還可以給這些客戶端貼上標(biāo)簽例如用戶姓名,聊天等級(jí)&middot;&middot;&middot;&middot;&middot;&middot;,就可以標(biāo)識(shí)每個(gè)客戶端啦。在這里由于篇幅原因,我沒有寫出所有功能,因?yàn)槭褂迷? NIO 實(shí)在是不太便捷。

什么事IO流

我相信你們都是直接滑下來看這里的,我在寫這段代碼的時(shí)候也非常痛苦,甚至有點(diǎn)厭煩 Java 原生的 NIO 編程。實(shí)際上我們在日常開發(fā)中很少直接用 NIO  進(jìn)行編程,通常都會(huì)用 Netty,Mina 這種服務(wù)器框架,它們都是很好地 NIO 技術(shù),對(duì) Java 原生的 NIO  進(jìn)行了上層的封裝、優(yōu)化,簡化開發(fā)難度,但是在學(xué)習(xí)框架之前,我們需要了解它底層原生的技術(shù),就像 Spring AOP 的動(dòng)態(tài)代理,Spring IOC 容器的  Map 容器存儲(chǔ)對(duì)象,Netty 底層的 NIO 基礎(chǔ)

“什么事IO流”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI