Java NIO 基礎(chǔ)知識(shí)
已經(jīng)很久沒(méi)有更新文章了,對(duì)于大家的期待,真的非常抱歉。
一直想著寫一些 NIO 相關(guān)的文章的,不過(guò)進(jìn)度有點(diǎn)慢。這篇文章也不算是正式介紹 NIO 的,就是先介紹一些相關(guān)的知識(shí),畢竟底子才是最重要的。
我想著可能 NIO 會(huì)大概分為三篇文章。這篇算是基礎(chǔ)知識(shí)介紹,不一定直接和 NIO 相關(guān),但是多了解相關(guān)的知識(shí)總是好的;下一篇準(zhǔn)備介紹 Java NIO 和 NIO2;最后一篇介紹 Netty,誰(shuí)讓它牛逼到都遮住了 Java NIO 的光芒呢。
計(jì)劃是這么計(jì)劃的,后兩篇在哪里我也不知道,至少第一篇是這么成文了。
面向讀者:希望讀者已經(jīng)熟悉 IO 了,最好是或多或少聽(tīng)過(guò)一些 NIO 的故事。
前言
前言部分是科普,讀者可自行選擇是否閱讀這部分內(nèi)容。
為什么我們需要關(guān)心 NIO?我想很多業(yè)務(wù)猿都會(huì)有這個(gè)疑問(wèn)。
我在工作的前兩年對(duì)這個(gè)問(wèn)題也很不解,因?yàn)槟莻€(gè)時(shí)候我認(rèn)為自己已經(jīng)非常熟悉 IO 操作了,讀寫文件什么的都非常溜了,IO 包無(wú)非就是 File、RandomAccessFile、字節(jié)流、字符流這些,感覺(jué)沒(méi)什么好糾結(jié)的。最混亂的當(dāng)屬 InputStream/OutputStream 一大堆的類不知道誰(shuí)是誰(shuí),不過(guò)了解了裝飾者模式以后,也都輕松破解了。
在 Java 領(lǐng)域,一般性的文件操作確實(shí)只需要和 java.io 包打交道就可以了,尤其對(duì)于寫業(yè)務(wù)代碼的程序員來(lái)說(shuō)。不過(guò),當(dāng)你寫了兩三年代碼后,你的業(yè)務(wù)代碼可能已經(jīng)寫得很溜了,蒙著眼睛也能寫增刪改查了。這個(gè)時(shí)候,也許你會(huì)想要開(kāi)始了解更多的底層內(nèi)容,包括并發(fā)、JVM、分布式系統(tǒng)、各個(gè)開(kāi)源框架源碼實(shí)現(xiàn)等,處于這個(gè)階段的程序員會(huì)開(kāi)始認(rèn)識(shí)到 NIO 的用處,因?yàn)橄到y(tǒng)間通訊無(wú)處不在。
可能很多人不知道 Netty 或 Mina 有什么用?和 Tomcat 有什么區(qū)別?為什么我用 HTTP 請(qǐng)求就可以解決應(yīng)用間調(diào)用的問(wèn)題卻要使用 Netty?
當(dāng)然,這些問(wèn)題的答案很簡(jiǎn)單,就是為了提升性能。那意思是 Tomcat 性能不好?當(dāng)然不是,它們的使用場(chǎng)景就不一樣。當(dāng)初我也不知道 Nginx 擺在 Tomcat 前面有什么用,也是經(jīng)過(guò)實(shí)踐慢慢領(lǐng)悟到了那么些意思。
Nginx 是 web 服務(wù)器,Tomcat/Jetty 是應(yīng)用服務(wù)器,Netty 是通訊工具。
也許你現(xiàn)在還不知道 NIO 有什么用,但是一定不要放棄學(xué)習(xí)它。
緩沖區(qū)操作
緩沖區(qū)是 NIO 操作的核心,本質(zhì)上 NIO 操作就是緩沖區(qū)操作。
寫操作是將緩沖區(qū)的數(shù)據(jù)排干,如將數(shù)據(jù)從緩沖區(qū)持久化到磁盤中。
讀操作是將數(shù)據(jù)填充到緩沖區(qū)中,以便應(yīng)用程序后續(xù)使用數(shù)據(jù)。
當(dāng)然,我們這里說(shuō)的緩沖區(qū)是指用戶空間的緩沖區(qū)。

簡(jiǎn)單分析下上圖。應(yīng)用程序發(fā)出讀操作后,內(nèi)核向磁盤控制器發(fā)送命令,要求磁盤返回相應(yīng)數(shù)據(jù),磁盤控制器通過(guò) DMA 直接將數(shù)據(jù)發(fā)送到內(nèi)核緩沖區(qū)。一旦內(nèi)核緩沖區(qū)滿了,內(nèi)核即把數(shù)據(jù)拷貝到請(qǐng)求數(shù)據(jù)的進(jìn)程指定的緩沖區(qū)中。
DMA:?Direct?Memory?Access
Wikipedia:直接內(nèi)存訪問(wèn)是計(jì)算機(jī)科學(xué)中的一種內(nèi)存訪問(wèn)技術(shù)。它允許某些電腦內(nèi)部的硬件子系統(tǒng)(電腦外設(shè)),可以獨(dú)立地直接讀寫系統(tǒng)內(nèi)存,而不需中央處理器(CPU)介入處理 。在同等程度的處理器負(fù)擔(dān)下,DMA 是一種快速的數(shù)據(jù)傳送方式。很多硬件的系統(tǒng)會(huì)使用 DMA,包含硬盤控制器、繪圖顯卡、網(wǎng)卡和聲卡。
也就是說(shuō),磁盤控制器可以在不用 CPU 的幫助下就將數(shù)據(jù)從磁盤寫到內(nèi)存中,畢竟讓 CPU 等待 IO 操作完成是一種浪費(fèi)
很容易看出來(lái),數(shù)據(jù)先到內(nèi)核,然后再?gòu)膬?nèi)核復(fù)制到用戶空間緩沖區(qū)的做法并不高效,下面簡(jiǎn)單說(shuō)說(shuō)為什么需要這么設(shè)計(jì)。
首先,用戶空間運(yùn)行的代碼是不可以直接訪問(wèn)硬件的,需要由內(nèi)核空間來(lái)負(fù)責(zé)和硬件通訊,內(nèi)核空間由操作系統(tǒng)控制。
其次,磁盤存儲(chǔ)的是固定大小的數(shù)據(jù)塊,磁盤按照扇區(qū)來(lái)組織數(shù)據(jù),而用戶進(jìn)程請(qǐng)求的一般都是任意大小的數(shù)據(jù)塊,所以需要由內(nèi)核來(lái)負(fù)責(zé)協(xié)調(diào),內(nèi)核會(huì)負(fù)責(zé)組裝、拆解數(shù)據(jù)。
內(nèi)核空間會(huì)對(duì)數(shù)據(jù)進(jìn)行緩存和預(yù)讀取,所以,如果用戶進(jìn)程需要的數(shù)據(jù)剛好在內(nèi)核空間中,直接拷貝過(guò)來(lái)就可以了。如果內(nèi)核空間沒(méi)有用戶進(jìn)程需要的數(shù)據(jù)的話,需要掛起用戶進(jìn)程,等待數(shù)據(jù)準(zhǔn)備好。
虛擬內(nèi)存
這個(gè)概念大家都懂,這里就繼續(xù)啰嗦一下了,虛擬內(nèi)存是計(jì)算機(jī)系統(tǒng)內(nèi)存管理的一種技術(shù)。前面說(shuō)的緩存區(qū)操作看似簡(jiǎn)單,但是具體到底層細(xì)節(jié),還是蠻復(fù)雜的。
下面的描述,我盡量保證準(zhǔn)確,但是不會(huì)展開(kāi)得太具體,因?yàn)樘摂M內(nèi)存還是蠻復(fù)雜的,要完全介紹清楚,恐怕需要很大的篇幅,如果讀者對(duì)這方面的內(nèi)容感興趣的話,建議讀者尋找更加專業(yè)全面的介紹資料,如《深入理解計(jì)算機(jī)系統(tǒng)》。
物理內(nèi)存被組織成一個(gè)很大的數(shù)組,每個(gè)單元是一個(gè)字節(jié)大小,然后每個(gè)字節(jié)都有一個(gè)唯一的物理地址,這應(yīng)該很好理解。
虛擬內(nèi)存是對(duì)物理內(nèi)存的抽象,它使得應(yīng)用程序認(rèn)為它自己擁有連續(xù)可用的內(nèi)存(一個(gè)連續(xù)完整的地址空間),而實(shí)際上,應(yīng)用程序得到的全部?jī)?nèi)存其實(shí)是一個(gè)假象,它通常會(huì)被分隔成多個(gè)物理內(nèi)存碎片(后面說(shuō)的頁(yè)),還有部分暫時(shí)存儲(chǔ)在外部磁盤存儲(chǔ)器上,在需要時(shí)進(jìn)行換入換出。
舉個(gè)例子,在 32 位系統(tǒng)中,每個(gè)應(yīng)用程序能訪問(wèn)到的內(nèi)存是 4G(32 位系統(tǒng)的最大尋址空間 2^32),這里的 4G 就是虛擬內(nèi)存,每個(gè)程序都以為自己擁有連續(xù)的 4G 空間的內(nèi)存,即使我們的計(jì)算機(jī)只有 2G 的物理內(nèi)存。也就是說(shuō),對(duì)于機(jī)器上同時(shí)運(yùn)行的多個(gè)應(yīng)用程序,每個(gè)程序都以為自己能得到連續(xù)的 4G 的內(nèi)存。這中間就是使用了虛擬內(nèi)存。
我們從概念上看,虛擬內(nèi)存也被組織成一個(gè)很大的數(shù)組,每個(gè)單元也是一個(gè)字節(jié)大小,每個(gè)字節(jié)都有唯一的虛擬地址。它被存儲(chǔ)于磁盤上,物理內(nèi)存是它的緩存。
物理內(nèi)存作為虛擬內(nèi)存的緩存,當(dāng)然不是以字節(jié)為單位進(jìn)行組織的,那樣效率太低了,它們之間是以頁(yè)(page)進(jìn)行緩存的。虛擬內(nèi)存被分割為一個(gè)個(gè)虛擬頁(yè),物理內(nèi)存也被分割為一個(gè)個(gè)物理頁(yè),這兩個(gè)頁(yè)的大小應(yīng)該是一致的,通常是 4KB - 2MB。
舉個(gè)例子,看下圖:

進(jìn)程 1 現(xiàn)在有 8 個(gè)虛擬頁(yè),其中有 2 個(gè)虛擬頁(yè)緩存在主存中,6 個(gè)還在磁盤上,需要的時(shí)候再讀入主存中;進(jìn)程 2 有 7 個(gè)虛擬頁(yè),其中 4 個(gè)緩存在主存中,3 個(gè)還在磁盤上。
在 CPU 讀取內(nèi)存數(shù)據(jù)的時(shí)候,給出的是虛擬地址,將一個(gè)虛擬地址轉(zhuǎn)換為物理地址的任務(wù)我們稱之為地址翻譯。在主存中的查詢表存放了虛擬地址到物理地址的映射關(guān)系,表的內(nèi)容由操作系統(tǒng)維護(hù)。CPU 需要訪問(wèn)內(nèi)存時(shí),CPU 上有一個(gè)叫做內(nèi)存管理單元的硬件會(huì)先去查詢真實(shí)的物理地址,然后再到指定的物理地址讀取數(shù)據(jù)。
上面說(shuō)的那個(gè)查詢表,我們稱之為頁(yè)表,虛擬內(nèi)存系統(tǒng)通過(guò)頁(yè)表來(lái)判斷一個(gè)虛擬頁(yè)是否已經(jīng)緩存在了主存中。如果是,頁(yè)表會(huì)負(fù)責(zé)到物理頁(yè)的映射;如果不命中,也就是我們經(jīng)常會(huì)見(jiàn)到的概念缺頁(yè),對(duì)應(yīng)的英文是 page fault,系統(tǒng)首先判斷這個(gè)虛擬頁(yè)存放在磁盤的哪個(gè)位置,然后在物理內(nèi)存中選擇一個(gè)犧牲頁(yè),并將虛擬頁(yè)從磁盤復(fù)制到內(nèi)存中,替換這個(gè)犧牲頁(yè)。
在磁盤和內(nèi)存之間傳送頁(yè)的活動(dòng)叫做交換(swapping)或者頁(yè)面調(diào)度(paging)。
下面,簡(jiǎn)單介紹下虛擬內(nèi)存帶來(lái)的好處。
SRAM緩存:表示位于 CPU 和主存之間的 L1、L2 和 L3 高速緩存。
DRAM緩存:表示虛擬內(nèi)存系統(tǒng)的緩存,緩存虛擬頁(yè)到主存中。
物理內(nèi)存訪問(wèn)速度比高速緩存要慢 10 倍左右,而磁盤要比物理內(nèi)存慢大約 100000 倍。所以,DRAM 的緩存不命中比 SRAM 緩存不命中代價(jià)要大得多,因?yàn)?DRAM 緩存一旦不命中,就需要到磁盤加載虛擬頁(yè)。而 SRAM 緩存不命中,通常由 DRAM 的主存來(lái)服務(wù)。而從磁盤的一個(gè)扇區(qū)讀取第一個(gè)字節(jié)的時(shí)間開(kāi)銷比起讀這個(gè)扇區(qū)中連續(xù)的字節(jié)要慢大約 100000 倍。
了解 Kafka 的讀者應(yīng)該知道,消息在磁盤中的順序存儲(chǔ)對(duì)于 Kafka 的性能至關(guān)重要。
結(jié)論就是,IO 的性能主要是由 DRAM 的緩存是否命中決定的。
內(nèi)存映射文件
英文名是?Memory Mapped Files,相信大家也都聽(tīng)過(guò)這個(gè)概念,在許多對(duì) IO 性能要求比較高的 java 應(yīng)用中會(huì)使用到,它是操作系統(tǒng)提供的支持,后面我們?cè)诮榻B NIO Buffer 的時(shí)候會(huì)碰到的?MappedByteBuffer?就是用來(lái)支持這一特性的。
是什么:
我們可以認(rèn)為內(nèi)存映射文件是一類特殊的文件,我們的 Java 程序可以直接從內(nèi)存中讀取到文件的內(nèi)容。它是通過(guò)將整個(gè)文件或文件的部分內(nèi)容映射到內(nèi)存頁(yè)中實(shí)現(xiàn)的,操作系統(tǒng)會(huì)負(fù)責(zé)加載需要的頁(yè),所以它的速度是非常快的。
優(yōu)勢(shì):
一旦我們將數(shù)據(jù)寫入到了內(nèi)存映射文件,即使我們的 JVM 掛掉了,操作系統(tǒng)依然會(huì)幫助我們將這部分內(nèi)存數(shù)據(jù)持久化到磁盤上。當(dāng)然了,如果是斷電的話,還是有可能會(huì)丟失數(shù)據(jù)的。
另外,它比較適合于處理大文件,因?yàn)椴僮飨到y(tǒng)只會(huì)在我們需要的頁(yè)不在內(nèi)存中時(shí)才會(huì)去加載頁(yè)數(shù)據(jù),而用其處理大量的小文件反而可能會(huì)造成頻繁的缺頁(yè)。
另一個(gè)重要的優(yōu)勢(shì)就是內(nèi)存共享。我們可以在多個(gè)進(jìn)程中同時(shí)使用同一個(gè)內(nèi)存映射文件,也算是一種進(jìn)程間協(xié)作的方式吧。想像下進(jìn)程間的數(shù)據(jù)通訊平時(shí)我們一般采用 Socket 來(lái)請(qǐng)求,而內(nèi)存共享至少可以帶來(lái) 10 倍以上的性能提升。
我們還沒(méi)有接觸到 NIO 的 Buffer,下面就簡(jiǎn)單地示意一下:
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class MemoryMappedFileInJava {
? ? private static int count = 10485760; //10 MB
? ? public static void main(String[] args) throws Exception {
? ? ? ? RandomAccessFile memoryMappedFile = new RandomAccessFile("largeFile.txt", "rw");
? ? ? ? // 將文件映射到內(nèi)存中,map 方法
? ? ? ? MappedByteBuffer out = memoryMappedFile.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, count);
? ? ? ? // 這一步的寫操作其實(shí)是寫到內(nèi)存中,并不直接操作文件
? ? ? ? for (int i = 0; i < count; i++) {
? ? ? ? ? ? out.put((byte) 'A');
? ? ? ? }
? ? ? ? System.out.println("Writing to Memory Mapped File is completed");
? ? ? ? // 這一步的讀操作讀的是內(nèi)存
? ? ? ? for (int i = 0; i < 10 ; i++) {
? ? ? ? ? ? System.out.print((char) out.get(i));
? ? ? ? }
? ? ? ? System.out.println("Reading from Memory Mapped File is completed");
? ? }
}
我們需要注意的一點(diǎn)就是,用于加載內(nèi)存映射文件的內(nèi)存是堆外內(nèi)存。
分散/聚集 IO
scatter/gather IO,個(gè)人認(rèn)為這個(gè)看上去很酷炫,實(shí)踐中比較難使用到。
分散/聚集 IO(另一種說(shuō)法是?vectored I/O 也就是向量 IO)是一種可以在單次操作中對(duì)多個(gè)緩沖區(qū)進(jìn)行輸入輸出的方法,可以把多個(gè)緩沖區(qū)的數(shù)據(jù)寫到單個(gè)數(shù)據(jù)流,也可以把單個(gè)數(shù)據(jù)流讀到多個(gè)緩沖區(qū)中。


非阻塞 IO
相信讀者在很多地方都看到過(guò)說(shuō) NIO 其實(shí)不是代表 New IO,而是 Non-Blocking IO,我們這里不糾結(jié)這個(gè)。我想之所以會(huì)有這個(gè)說(shuō)法,是因?yàn)樵?Java 1.4 第一次推出 NIO 的時(shí)候,提供了 Non-Blocking IO 的支持。
在理解非阻塞 IO 前,我們首先要明白,它的對(duì)立面?阻塞模式為什么不好。
比如說(shuō) InputStream.read 這個(gè)方法,一旦某個(gè)線程調(diào)用這個(gè)方法,那么就將一直阻塞在這里,直到數(shù)據(jù)傳輸完畢,返回 -1,或者由于其他錯(cuò)誤拋出了異常。
我們?cè)倌?web 服務(wù)器來(lái)說(shuō),阻塞模式的話,每個(gè)網(wǎng)絡(luò)連接進(jìn)來(lái),我們都需要開(kāi)啟一個(gè)線程來(lái)讀取請(qǐng)求數(shù)據(jù),然后到后端進(jìn)行處理,處理結(jié)束后將數(shù)據(jù)寫回網(wǎng)絡(luò)連接,這整個(gè)流程需要一個(gè)獨(dú)立的線程來(lái)做這件事。那就意味著,一旦請(qǐng)求數(shù)量多了以后,需要?jiǎng)?chuàng)建大量的線程,大量的線程必然帶來(lái)創(chuàng)建線程、切換線程的開(kāi)銷,更重要的是,要給每個(gè)線程都分配一部分內(nèi)存,會(huì)使得內(nèi)存迅速被消耗殆盡。我們說(shuō)多線程是性能利器,但是這就是過(guò)多的線程導(dǎo)致系統(tǒng)完全消化不了了。
通常,我們可以將 IO 分為兩類:面向數(shù)據(jù)塊(block-oriented)的 IO 和面向流(stream-oriented)的 IO。比如文件的讀寫就是面向數(shù)據(jù)塊的,讀取鍵盤輸入或往網(wǎng)絡(luò)中寫入數(shù)據(jù)就是面向流的。
注意,這節(jié)混著用了流和通道這兩個(gè)詞,提出來(lái)這點(diǎn)是希望不會(huì)對(duì)讀者產(chǎn)生困擾。
面向流的 IO 往往是比較慢的,如網(wǎng)絡(luò)速度比較慢、需要一直等待用戶新的輸入等。
這個(gè)時(shí)候,我們可以用一個(gè)線程來(lái)處理多個(gè)流,讓這個(gè)線程負(fù)責(zé)一直輪詢這些流的狀態(tài),當(dāng)有的流有數(shù)據(jù)到來(lái)后,進(jìn)行相應(yīng)處理,也可以將數(shù)據(jù)交給其他子線程來(lái)處理,這個(gè)線程繼續(xù)輪詢。
問(wèn)題來(lái)了,不斷地輪詢也會(huì)帶來(lái)資源浪費(fèi)呀,尤其是當(dāng)一個(gè)線程需要輪詢很多的數(shù)據(jù)流的時(shí)候。
現(xiàn)代操作系統(tǒng)提供了一個(gè)叫做?readiness selection?的功能,我們讓操作系統(tǒng)來(lái)監(jiān)控一個(gè)集合中的所有的通道,當(dāng)有的通道數(shù)據(jù)準(zhǔn)備好了以后,就可以直接到這個(gè)通道獲取數(shù)據(jù)。當(dāng)然,操作系統(tǒng)不會(huì)通知我們,但是我們?nèi)?wèn)操作系統(tǒng)的時(shí)候,它會(huì)知道告訴我們通道 N 已經(jīng)準(zhǔn)備好了,而不需要自己去輪詢(后面我們會(huì)看到,還要自己輪詢的 select 和 poll)。
后面我們?cè)诮榻B Java NIO 的時(shí)候會(huì)說(shuō)到 Selector,對(duì)應(yīng)類 java.nio.channels.Selector,這個(gè)就是 java 對(duì) readiness selection 的支持。這樣一來(lái),我們的一個(gè)線程就可以更加高效地管理多個(gè)通道了。

上面這張圖我想大家也都可能看過(guò),就是用一個(gè) Selector 來(lái)管理多個(gè) Channel,實(shí)現(xiàn)了一個(gè)線程管理多個(gè)連接。說(shuō)到底,其實(shí)就是解決了我們前面說(shuō)的阻塞模式下線程創(chuàng)建過(guò)多的問(wèn)題。
在 Java 中,繼承自?SelectableChannel?的子類就是實(shí)現(xiàn)了非阻塞 IO 的,我們可以看到主要有 socket IO 中的 DatagramChannel 和 SocketChannel,而 FileChannel 并沒(méi)有繼承它。所以,文件 IO 是不支持非阻塞模式的。
在系統(tǒng)實(shí)現(xiàn)上,POSIX 提供了?select?和?poll?兩種方式。它們兩個(gè)最大的區(qū)別在于持有句柄的數(shù)量上,select 最多只支持到 FD_SETSIZE(一般常見(jiàn)的是 1024),顯然很多場(chǎng)景都會(huì)超過(guò)這個(gè)數(shù)量。而 poll 我們想創(chuàng)建多少就創(chuàng)建多少。它們都有一個(gè)共同的缺點(diǎn),那就是當(dāng)有任務(wù)完成后,我們只能知道有幾個(gè)任務(wù)完成了,而不知道具體是哪幾個(gè)句柄,所以還需要進(jìn)行一次掃描。
正是由于 select 和 poll 的不足,所以催生了以下幾個(gè)實(shí)現(xiàn)。BSD& OS X 中的 kqueue,Solaris 中的 /dev/poll,還有?Linux 中的 epoll。
Windows 沒(méi)有提供額外的實(shí)現(xiàn),只能使用 select。
在不同的操作系統(tǒng)上,JDK 分別選擇相應(yīng)的系統(tǒng)支持的非阻塞實(shí)現(xiàn)方式。
異步 IO
我們知道 Java 1.4 引入了 New IO,從 Java 7 開(kāi)始,就不再是 New IO 了,而是 More New IO 來(lái)臨了,我們也稱之為 NIO2。
Java7 在 NIO 上帶來(lái)的最大的變化應(yīng)該就屬引入了 Asynchronous IO(異步 IO)。本來(lái)吧,異步 IO 早就提上日程了,可是大佬們沒(méi)有時(shí)間完成,所以才一直拖到了 java 7 的。廢話不多說(shuō),簡(jiǎn)單來(lái)看看異步 IO 是什么。
要說(shuō)異步 IO 是什么,當(dāng)然還得從 Non-Blocking IO 沒(méi)有解決的問(wèn)題入手。非阻塞 IO 很好用,它解決了阻塞式 IO 的等待問(wèn)題,但是它的缺點(diǎn)是需要我們?nèi)ポ喸儾拍艿玫浇Y(jié)果。
而異步 IO 可以解決這個(gè)問(wèn)題,線程只需要初始化一下,提供一個(gè)回調(diào)方法,然后就可以干其他的事情了。當(dāng)數(shù)據(jù)準(zhǔn)備好以后,系統(tǒng)會(huì)負(fù)責(zé)調(diào)用回調(diào)方法。
異步 IO 最主要的特點(diǎn)就是回調(diào),其實(shí)回調(diào)在我們?nèi)粘5拇a中也是非常常見(jiàn)的。
最簡(jiǎn)單的方法就是設(shè)計(jì)一個(gè)線程池,池中的線程負(fù)責(zé)完成一個(gè)個(gè)阻塞式的操作,一旦一個(gè)操作完成,那么就調(diào)用回調(diào)方法。比如 web 服務(wù)器中,我們前面已經(jīng)說(shuō)過(guò)不能每來(lái)一個(gè)請(qǐng)求就新開(kāi)一個(gè)線程,我們可以設(shè)計(jì)一個(gè)線程池,在線程池外用一個(gè)線程來(lái)接收請(qǐng)求,然后將要完成的任務(wù)交給線程池中的線程并提供一個(gè)回調(diào)方法,這樣這個(gè)線程就可以去干其他的事情了,如繼續(xù)處理其他的請(qǐng)求。等任務(wù)完成后,池中的線程就可以調(diào)用回調(diào)方法進(jìn)行通知了。
另外一種方式就是自己不設(shè)計(jì)線程池,讓操作系統(tǒng)幫我們實(shí)現(xiàn)。流程也是基本一樣的,提供給操作系統(tǒng)回調(diào)方法,然后就可以干其他事情了,等操作完成后,操作系統(tǒng)會(huì)負(fù)責(zé)回調(diào)。這種方式的缺點(diǎn)就是依賴于操作系統(tǒng)的具體實(shí)現(xiàn),不過(guò)也有它的一些優(yōu)勢(shì)。
首先,我們自己設(shè)計(jì)處理任務(wù)的線程池的話,我們需要掌握好線程池的大小,不能太大,也不能太小,這往往需要憑我們的經(jīng)驗(yàn);其次,讓操作系統(tǒng)來(lái)做這件事情的話,操作系統(tǒng)可以在一些場(chǎng)景中幫助我們優(yōu)化性能,如文件 IO 過(guò)程中幫助更快找到需要的數(shù)據(jù)。
操作系統(tǒng)對(duì)異步 IO 的實(shí)現(xiàn)也有很多種方式,主要有以下 3 中:
Linux AIO:由 Linux 內(nèi)核提供支持
POSIX AIO:Linux,Mac OS X(現(xiàn)在該叫 Mac OS 了),BSD,solaris 等都支持,在 Linux 中是通過(guò)?glibc?來(lái)提供支持的。
Windows:提供了一個(gè)叫做?completion ports?的機(jī)制。
這篇文章?asynchronous disk I/O?的作者表示,在類 unix 的幾個(gè)系統(tǒng)實(shí)現(xiàn)中,限制太多,實(shí)現(xiàn)的質(zhì)量太差,還不如自己用線程池進(jìn)行管理異步操作。
而 Windows 系統(tǒng)下提供的異步 IO 的實(shí)現(xiàn)方式有點(diǎn)不一樣。它首先讓線程池中的線程去自旋調(diào)用?GetQueuedCompletionStatus.aspx) 方法,判斷是否就緒。然后,讓任務(wù)跑起來(lái),但是需要提供特定的參數(shù)來(lái)告訴執(zhí)行任務(wù)的線程,讓線程執(zhí)行完成后將結(jié)果通知到線程池中。一旦任務(wù)完成,操作系統(tǒng)會(huì)將線程池中阻塞在 GetQueuedCompletionStatus 方法的線程喚醒,讓其進(jìn)行后續(xù)的結(jié)果處理。
Windows?智能地喚醒那些執(zhí)行 GetQueuedCompletionStatus 方法的線程,以讓線程池中活躍的線程數(shù)始終保持在合理的水平。這樣就不至于創(chuàng)建太多的線程,降低線程切換的開(kāi)銷。
Java 7 在異步 IO 的實(shí)現(xiàn)上,如果是 Linux 或者其他類 Unix 系統(tǒng)上,是采用自建線程池實(shí)現(xiàn)的,如果是 Windows 系統(tǒng)上,是采用系統(tǒng)提供的 completion ports 來(lái)實(shí)現(xiàn)的。
所以,在非阻塞 IO 和異步 IO 之間,我們應(yīng)該怎么選擇呢?
如果是文件 IO,我們沒(méi)得選,只能選擇異步 IO。
如果是 Socket IO,在類 unix 系統(tǒng)下我們應(yīng)該選擇使用非阻塞 IO,Netty 是基于非阻塞模式的;在 Windows 中我們應(yīng)該使用異步 IO。
當(dāng)然了,Java 的存在就是為了實(shí)現(xiàn)平臺(tái)無(wú)關(guān)化,所以,其實(shí)不需要我們選擇,了解這些權(quán)當(dāng)讓自己漲點(diǎn)知識(shí)吧。
總結(jié)
和其他幾篇文章一樣,也沒(méi)什么好總結(jié)的,要說(shuō)的都在文中了,希望讀者能學(xué)到點(diǎn)東西吧。
如果哪里說(shuō)得不對(duì)了,我想也是正常的,我這些年寫的都是 Java,對(duì)于底層了解得愈發(fā)的少了,所以如果讀者發(fā)現(xiàn)有什么不合理的內(nèi)容,非常希望讀者可以提出來(lái)。
Reference
《深入計(jì)算機(jī)系統(tǒng)》
《Java NIO》
《Java IO, NIO and NIO.2》