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