以Java的视角来聊聊BIO、NIO与AIO的区别
首先來說一下什么是I/O?
在計(jì)算機(jī)系統(tǒng)中I/O就是輸入(Input)和輸出(Output)的意思,針對不同的操作對象,可以劃分為磁盤I/O模型,網(wǎng)絡(luò)I/O模型,內(nèi)存映射I/O, Direct I/O、數(shù)據(jù)庫I/O等,只要具有輸入輸出類型的交互系統(tǒng)都可以認(rèn)為是I/O系統(tǒng),也可以說I/O是整個操作系統(tǒng)數(shù)據(jù)交換與人機(jī)交互的通道,這個概念與選用的開發(fā)語言沒有關(guān)系,是一個通用的概念。
在如今的系統(tǒng)中I/O卻擁有很重要的位置,現(xiàn)在系統(tǒng)都有可能處理大量文件,大量數(shù)據(jù)庫操作,而這些操作都依賴于系統(tǒng)的I/O性能,也就造成了現(xiàn)在系統(tǒng)的瓶頸往往都是由于I/O性能造成的。因此,為了解決磁盤I/O性能慢的問題,系統(tǒng)架構(gòu)中添加了緩存來提高響應(yīng)速度;或者有些高端服務(wù)器從硬件級入手,使用了固態(tài)硬盤(SSD)來替換傳統(tǒng)機(jī)械硬盤;在大數(shù)據(jù)方面,Spark越來越多的承擔(dān)了實(shí)時性計(jì)算任務(wù),而傳統(tǒng)的Hadoop體系則大多應(yīng)用在了離線計(jì)算與大量數(shù)據(jù)存儲的場景,這也是由于磁盤I/O性能遠(yuǎn)不如內(nèi)存I/O性能而造成的格局(Spark更多的使用了內(nèi)存,而MapReduece更多的使用了磁盤)。因此,一個系統(tǒng)的優(yōu)化空間,往往都在低效率的I/O環(huán)節(jié)上,很少看到一個系統(tǒng)CPU、內(nèi)存的性能是其整個系統(tǒng)的瓶頸。也正因?yàn)槿绱?#xff0c;Java在I/O上也一直在做持續(xù)的優(yōu)化,從JDK 1.4開始便引入了NIO模型,大大的提高了以往BIO模型下的操作效率。
這里先給出BIO、NIO、AIO的基本定義與類比描述:
-
BIO (Blocking I/O):同步阻塞I/O模式,數(shù)據(jù)的讀取寫入必須阻塞在一個線程內(nèi)等待其完成。這里使用那個經(jīng)典的燒開水例子,這里假設(shè)一個燒開水的場景,有一排水壺在燒開水,BIO的工作模式就是, 叫一個線程停留在一個水壺那,直到這個水壺?zé)_,才去處理下一個水壺。但是實(shí)際上線程在等待水壺?zé)_的時間段什么都沒有做。
-
NIO (New I/O):同時支持阻塞與非阻塞模式,但這里我們以其同步非阻塞I/O模式來說明,那么什么叫做同步非阻塞?如果還拿燒開水來說,NIO的做法是叫一個線程不斷的輪詢每個水壺的狀態(tài),看看是否有水壺的狀態(tài)發(fā)生了改變,從而進(jìn)行下一步的操作。
-
AIO ( Asynchronous I/O):異步非阻塞I/O模型。異步非阻塞與同步非阻塞的區(qū)別在哪里?異步非阻塞無需一個線程去輪詢所有IO操作的狀態(tài)改變,在相應(yīng)的狀態(tài)改變后,系統(tǒng)會通知對應(yīng)的線程來處理。對應(yīng)到燒開水中就是,為每個水壺上面裝了一個開關(guān),水燒開之后,水壺會自動通知我水燒開了。
進(jìn)程中的IO調(diào)用步驟大致可以分為以下四步:?
進(jìn)程向操作系統(tǒng)請求數(shù)據(jù) ;
操作系統(tǒng)把外部數(shù)據(jù)加載到內(nèi)核的緩沖區(qū)中;?
操作系統(tǒng)把內(nèi)核的緩沖區(qū)拷貝到進(jìn)程的緩沖區(qū) ;
進(jìn)程獲得數(shù)據(jù)完成自己的功能 ;
當(dāng)操作系統(tǒng)在把外部數(shù)據(jù)放到進(jìn)程緩沖區(qū)的這段時間(即上述的第二,三步),如果應(yīng)用進(jìn)程是掛起等待的,那么就是同步IO,反之,就是異步IO,也就是AIO 。
這是最基本與簡單的I/O操作方式,其根本特性是做完一件事再去做另一件事,一件事一定要等前一件事做完,這很符合程序員傳統(tǒng)的順序來開發(fā)思想,因此BIO模型程序開發(fā)起來較為簡單,易于把握。
但是BIO如果需要同時做很多事情(例如同時讀很多文件,處理很多tcp請求等),就需要系統(tǒng)創(chuàng)建很多線程來完成對應(yīng)的工作,因?yàn)锽IO模型下一個線程同時只能做一個工作,如果線程在執(zhí)行過程中依賴于需要等待的資源,那么該線程會長期處于阻塞狀態(tài),我們知道在整個操作系統(tǒng)中,線程是系統(tǒng)執(zhí)行的基本單位,在BIO模型下的線程 阻塞就會導(dǎo)致系統(tǒng)線程的切換,從而對整個系統(tǒng)性能造成一定的影響。當(dāng)然如果我們只需要創(chuàng)建少量可控的線程,那么采用BIO模型也是很好的選擇,但如果在需要考慮高并發(fā)的web或者tcp服務(wù)器中采用BIO模型就無法應(yīng)對了,如果系統(tǒng)開辟成千上萬的線程,那么CPU的執(zhí)行時機(jī)都會浪費(fèi)在線程的切換中,使得線程的執(zhí)行效率大大降低。此外,關(guān)于線程這里說一句題外話,在系統(tǒng)開發(fā)中線程的生命周期一定要準(zhǔn)確控制,在需要一定規(guī)模并發(fā)的情形下,盡量使用線程池來確保線程創(chuàng)建數(shù)目在一個合理的范圍之內(nèi),切莫編寫線程數(shù)量創(chuàng)建上限的代碼。
關(guān)于NIO,國內(nèi)有很多技術(shù)博客將英文翻譯成No-Blocking I/O,非阻塞I/O模型 ,當(dāng)然這樣就與BIO形成了鮮明的特性對比。NIO本身是基于事件驅(qū)動的思想來實(shí)現(xiàn)的,其目的就是解決BIO的大并發(fā)問題,在BIO模型中,如果需要并發(fā)處理多個I/O請求,那就需要多線程來支持,NIO使用了多路復(fù)用器機(jī)制,以socket使用來說,多路復(fù)用器通過不斷輪詢各個連接的狀態(tài),只有在socket有流可讀或者可寫時,應(yīng)用程序才需要去處理它,在線程的使用上,就不需要一個連接就必須使用一個處理線程了,而是只是有效請求時(確實(shí)需要進(jìn)行I/O處理時),才會使用一個線程去處理,這樣就避免了BIO模型下大量線程處于阻塞等待狀態(tài)的情景。
相對于BIO的流,NIO抽象出了新的通道(Channel)作為輸入輸出的通道,并且提供了緩存(Buffer)的支持,在進(jìn)行讀操作時,需要使用Buffer分配空間,然后將數(shù)據(jù)從Channel中讀入Buffer中,對于Channel的寫操作,也需要現(xiàn)將數(shù)據(jù)寫入Buffer,然后將Buffer寫入Channel中。
如下是NIO方式進(jìn)行文件拷貝操作的示例,見下圖:
通過比較New IO的使用方式我們可以發(fā)現(xiàn),新的IO操作不再面向 Stream來進(jìn)行操作了,改為了通道Channel,并且使用了更加靈活的緩存區(qū)類Buffer,Buffer只是緩存區(qū)定義接口, 根據(jù)需要,我們可以選擇對應(yīng)類型的緩存區(qū)實(shí)現(xiàn)類。在java NIO編程中,我們需要理解以下3個對象Channel、Buffer和Selector。
-
Channel
首先說一下Channel,國內(nèi)大多翻譯成“通道”。Channel和IO中的Stream(流)是差不多一個等級的。只不過Stream是單向的,譬如:InputStream, OutputStream。而Channel是雙向的,既可以用來進(jìn)行讀操作,又可以用來進(jìn)行寫操作,NIO中的Channel的主要實(shí)現(xiàn)有:FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel;通過看名字就可以猜出個所以然來:分別可以對應(yīng)文件IO、UDP和TCP(Server和Client)。
-
Buffer
NIO中的關(guān)鍵Buffer實(shí)現(xiàn)有:ByteBuffer、CharBuffer、DoubleBuffer、 FloatBuffer、IntBuffer、 LongBuffer,、ShortBuffer,分別對應(yīng)基本數(shù)據(jù)類型: byte、char、double、 float、int、 long、 short。當(dāng)然NIO中還有MappedByteBuffer, HeapByteBuffer, DirectByteBuffer等這里先不具體陳述其用法細(xì)節(jié)。
說一下 DirectByteBuffer 與 HeapByteBuffer 的區(qū)別?
它們 ByteBuffer 分配內(nèi)存的兩種方式。HeapByteBuffer 顧名思義其內(nèi)存空間在 JVM 的 heap(堆)上分配,可以看做是 jdk 對于 byte[] 數(shù)組的封裝;而 DirectByteBuffer 則直接利用了系統(tǒng)接口進(jìn)行內(nèi)存申請,其內(nèi)存分配在c heap 中,這樣就減少了內(nèi)存之間的拷貝操作,如此一來,在使用 DirectByteBuffer 時,系統(tǒng)就可以直接從內(nèi)存將數(shù)據(jù)寫入到 Channel 中,而無需進(jìn)行 Java 堆的內(nèi)存申請,復(fù)制等操作,提高了性能。既然如此,為什么不直接使用 DirectByteBuffer,還要來個 HeapByteBuffer?原因在于, DirectByteBuffer 是通過full gc來回收內(nèi)存的,DirectByteBuffer會自己檢測情況而調(diào)用 system.gc(),但是如果參數(shù)中使用了 DisableExplicitGC 那么就無法回收該快內(nèi)存了,-XX:+DisableExplicitGC標(biāo)志自動將 System.gc() 調(diào)用轉(zhuǎn)換成一個空操作,就是應(yīng)用中調(diào)用 System.gc() 會變成一個空操作,那么如果設(shè)置了就需要我們手動來回收內(nèi)存了,所以DirectByteBuffer使用起來相對于完全托管于 java 內(nèi)存管理的Heap ByteBuffer 來說更復(fù)雜一些,如果用不好可能會引起OOM。Direct ByteBuffer 的內(nèi)存大小受 -XX:MaxDirectMemorySize JVM 參數(shù)控制(默認(rèn)大小64M),在 DirectByteBuffer 申請內(nèi)存空間達(dá)到該設(shè)置大小后,會觸發(fā) Full GC。
-
Selector
Selector 是NIO相對于BIO實(shí)現(xiàn)多路復(fù)用的基礎(chǔ),Selector 運(yùn)行單線程處理多個 Channel,如果你的應(yīng)用打開了多個通道,但每個連接的流量都很低,使用 Selector 就會很方便。例如在一個聊天服務(wù)器中。要使用 Selector , 得向 Selector 注冊 Channel,然后調(diào)用它的 select() 方法。這個方法會一直阻塞到某個注冊的通道有事件就緒。一旦這個方法返回,線程就可以處理這些事件,事件的例子有如新的連接進(jìn)來、數(shù)據(jù)接收等。
這里我們再來看一個NIO模型下的TCP服務(wù)器的實(shí)現(xiàn),我們可以看到Selector 正是NIO模型下 TCP Server 實(shí)現(xiàn)IO復(fù)用的關(guān)鍵,請仔細(xì)理解下段代碼while循環(huán)中的邏輯,見下圖:
Java AIO就是Java作為對異步IO提供支持的NIO.2 ,Java NIO2 (JSR 203)定義了更多的 New I/O APIs, 提案2003提出,直到2011年才發(fā)布, 最終在JDK 7中才實(shí)現(xiàn)。JSR 203除了提供更多的文件系統(tǒng)操作API(包括可插拔的自定義的文件系統(tǒng)), 還提供了對socket和文件的異步 I/O操作。 同時實(shí)現(xiàn)了JSR-51提案中的socket channel全部功能,包括對綁定, option配置的支持以及多播multicast的實(shí)現(xiàn)。
從編程模式上來看AIO相對于NIO的區(qū)別在于,NIO需要使用者線程不停的輪詢IO對象,來確定是否有數(shù)據(jù)準(zhǔn)備好可以讀了,而AIO則是在數(shù)據(jù)準(zhǔn)備好之后,才會通知數(shù)據(jù)使用者,這樣使用者就不需要不停地輪詢了。當(dāng)然AIO的異步特性并不是Java實(shí)現(xiàn)的偽異步,而是使用了系統(tǒng)底層API的支持,在Unix系統(tǒng)下,采用了epoll IO模型,而windows便是使用了IOCP模型。關(guān)于Java AIO,本篇只做一個拋磚引玉的介紹,如果你在實(shí)際工作中用到了,那么可以參考Netty在高并發(fā)下使用AIO的相關(guān)技術(shù)。
總 結(jié)
? ??
IO實(shí)質(zhì)上與線程沒有太多的關(guān)系,但是不同的IO模型改變了應(yīng)用程序使用線程的方式,NIO與BIO的出現(xiàn)解決了很多BIO無法解決的并發(fā)問題,當(dāng)然任何技術(shù)拋開適用場景都是耍流氓,復(fù)雜的技術(shù)往往是為了解決簡單技術(shù)無法解決的問題而設(shè)計(jì)的,在系統(tǒng)開發(fā)中能用常規(guī)技術(shù)解決的問題,絕不用復(fù)雜技術(shù),否則大大增加系統(tǒng)代碼的維護(hù)難度,學(xué)習(xí)IT技術(shù)不是為了炫技,而是要實(shí)實(shí)在在解決問題。
總結(jié)
以上是生活随笔為你收集整理的以Java的视角来聊聊BIO、NIO与AIO的区别的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: spring事务配置,声明式事务管理和基
- 下一篇: Java 线程池(ThreadPoolE