理解Java NIO
摘要:?基礎(chǔ)概念 ? 緩沖區(qū)操作 緩沖區(qū)及操作是所有I/O的基礎(chǔ),進(jìn)程執(zhí)行I/O操作,歸結(jié)起來就是向操作系統(tǒng)發(fā)出請求,讓它要么把緩沖區(qū)里的數(shù)據(jù)排干(寫),要么把緩沖區(qū)填滿(讀)。如下圖 ? 內(nèi)核空間、用戶空間? 上圖簡單描述了數(shù)據(jù)從磁盤到用戶進(jìn)程的內(nèi)存區(qū)域移動的過程,其間涉及到了內(nèi)核空間與用戶空間。
基礎(chǔ)概念
? 緩沖區(qū)操作
緩沖區(qū)及操作是所有I/O的基礎(chǔ),進(jìn)程執(zhí)行I/O操作,歸結(jié)起來就是向操作系統(tǒng)發(fā)出請求,讓它要么把緩沖區(qū)里的數(shù)據(jù)排干(寫),要么把緩沖區(qū)填滿(讀)。如下圖
? 內(nèi)核空間、用戶空間?
上圖簡單描述了數(shù)據(jù)從磁盤到用戶進(jìn)程的內(nèi)存區(qū)域移動的過程,其間涉及到了內(nèi)核空間與用戶空間。這兩個空間有什么區(qū)別呢??
用戶空間就是常規(guī)進(jìn)程(如JVM)所在區(qū)域,用戶空間是非特權(quán)區(qū)域,如不能直接訪問硬件設(shè)備。內(nèi)核空間是操作系統(tǒng)所在區(qū)域,那肯定是有特權(quán)啦,如能與設(shè)備控制器通訊,控制用戶區(qū)域的進(jìn)程運(yùn)行狀態(tài)。進(jìn)程執(zhí)行I/O操作時,它執(zhí)行一個系統(tǒng)調(diào)用把控制權(quán)交由內(nèi)核。?
? 虛擬內(nèi)存?
? 內(nèi)存頁面調(diào)度?
5種I/O模型
說起I/O模型,網(wǎng)絡(luò)上有一個錯誤的概念,異步非阻塞/阻塞模型,其實(shí)異步根本就沒有阻不阻塞之說,異步模型就是異步模型。讓我們來看一看Richard Stevens在其UNIX網(wǎng)絡(luò)編程卷1中提出的5個I/O模型吧。
? 阻塞式I/O?
? 非阻塞式I/O?
? I/O復(fù)用(Java NIO就是這種模型)?
? 信號驅(qū)動式I/O?
? 異步I/O?
由POSIX術(shù)語定義,同步I/O操作導(dǎo)致請求進(jìn)程阻塞,直到I/O操作完成;異步I/O操作不導(dǎo)致請求進(jìn)程阻塞。5種模型中的前4種都屬于同步I/O模型。
Why NIO?
開始講NIO之前,了解為什么會有NIO,相比傳統(tǒng)流I/O的優(yōu)勢在哪,它可以用來做什么等等的問題,還是很有必要的。
傳統(tǒng)流I/O是基于字節(jié)的,所有I/O都被視為單個字節(jié)的移動;而NIO是基于塊的,大家可能猜到了,NIO的性能肯定優(yōu)于流I/O。沒錯!其性能的提高 要得益于其使用的結(jié)構(gòu)更接近操作系統(tǒng)執(zhí)行I/O的方式:通道和緩沖器。我們可以把它想象成一個煤礦,通道是一個包含煤層(數(shù)據(jù))的礦藏,而緩沖器則是派送 到礦藏的卡車。卡車載滿煤炭而歸,我們再從卡車上獲得煤炭。也就是說,我們并沒有直接和通道交互;我們只是和緩沖器交互,并把緩沖器派送到通道。通道要么 從緩沖器獲得數(shù)據(jù),要么向緩沖器發(fā)送數(shù)據(jù)。(這段比喻出自Java編程思想)
NIO的主要應(yīng)用在高性能、高容量服務(wù)端應(yīng)用程序,典型的有Apache Mina就是基于它的。
緩沖區(qū)?
緩沖區(qū)實(shí)質(zhì)上就是一個數(shù)組,但它不僅僅是一個數(shù)組,緩沖區(qū)還提供了對數(shù)據(jù)的結(jié)構(gòu)化訪問,而且還可以跟蹤系統(tǒng)的讀/寫進(jìn)程。為什么這么說呢?下面來看看緩沖區(qū)的細(xì)節(jié)。?
講緩沖區(qū)細(xì)節(jié)之前,我們先來看一下緩沖區(qū)“家譜”:
? 內(nèi)部細(xì)節(jié)?
緩沖區(qū)對象有四個基本屬性:?
o 容量Capacity:緩沖區(qū)能容納的數(shù)據(jù)元素的最大數(shù)量,在緩沖區(qū)創(chuàng)建時設(shè)定,無法更改?
o 上界Limit:緩沖區(qū)的第一個不能被讀或?qū)懙脑氐乃饕?
o 位置Position:下一個要被讀或?qū)懙脑氐乃饕?
o 標(biāo)記Mark:備忘位置,調(diào)用mark()來設(shè)定mark=position,調(diào)用reset()設(shè)定position=mark?
這四個屬性總是遵循這樣的關(guān)系:0<=mark<=position<=limit<=capacity。下圖是新創(chuàng)建的容量為10的緩沖區(qū)邏輯視圖:
buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o');
五次調(diào)用put后的緩沖區(qū):
buffer.put(0,(byte)'M').put((byte)'w');
調(diào)用絕對版本的put不影響position:?
現(xiàn)在緩沖區(qū)滿了,我們必須將其清空。我們想把這個緩沖區(qū)傳遞給一個通道,以使內(nèi)容能被全部寫出,但現(xiàn)在執(zhí)行g(shù)et()無疑會取出未定義的數(shù)據(jù)。我們必須將 posistion設(shè)為0,然后通道就會從正確的位置開始讀了,但讀到哪算讀完了呢?這正是limit引入的原因,它指明緩沖區(qū)有效內(nèi)容的未端。這個操作 在緩沖區(qū)中叫做翻轉(zhuǎn):buffer.flip()。?
rewind操作與flip相似,但不影響limit。?
將數(shù)據(jù)從輸入通道copy到輸出通道的過程應(yīng)該是這樣的:
? 創(chuàng)建緩沖區(qū)?
一般,新分配一個緩沖區(qū)是通過allocate方法的。如果你想提供自己的數(shù)組用做緩沖區(qū)的備份存儲器,請調(diào)用wrap方法。?
上面兩種方式創(chuàng)建的緩沖區(qū)都是間接的,間接的緩沖區(qū)使用備份數(shù)組(相關(guān)的方法有hasArray()、array()、arrayOffset())。?
? 復(fù)制緩沖區(qū)?
duplicate方法創(chuàng)建一個與原始緩沖區(qū)類似的緩沖區(qū),兩個緩沖區(qū)共享數(shù)據(jù)元素,不過它們擁有各自的position、limit、mark,如下圖:?
另一個方法,slice與duplicate相似,但slice方法創(chuàng)建一個從原始緩沖區(qū)的當(dāng)前位置開始的新緩沖區(qū),而且容量是原始緩沖區(qū)的剩余元素?cái)?shù)量(limit-position),見下圖。
? 字節(jié)緩沖區(qū)?
o 字節(jié)序?
為什么會有字節(jié)序?比如有1個int類型數(shù)字0x036fc5d9,它占4個字節(jié) ,那么在內(nèi)存中存儲時,有可能其最高字節(jié)03位于低位地址(大端字節(jié)順序),也有可能最低字節(jié)d9位于低位地址(小端字節(jié)順序)。?
在IP協(xié)議中規(guī)定了使用大端的網(wǎng)絡(luò)字節(jié)順序,所以我們必須先在本地主機(jī)字節(jié)順序和通用的網(wǎng)絡(luò)字節(jié)順序之間進(jìn)行轉(zhuǎn)換。java.nio中,字節(jié)順序由ByteOrder類封裝。?
在ByteBuffer中默認(rèn)字節(jié)序?yàn)锽yteBuffer.BIG_ENDIAN,不過byte為什么還需要字節(jié)序呢?ByteBuffer和其他基本 數(shù)據(jù)類型一樣,具有大量便利的方法用于獲取和存放緩沖區(qū)內(nèi)容,這些方法對字節(jié)進(jìn)行編碼或解碼的方式取決于ByteBuffer當(dāng)前字節(jié)序。?
o 直接緩沖區(qū)?
直接緩沖區(qū)是通過調(diào)用ByteBuffer.allocateDirect方法創(chuàng)建的。通常直接緩沖區(qū)是I/O操作的最好選擇,因?yàn)樗苊饬艘恍?fù)制過程;但可能也比間接緩沖區(qū)要花費(fèi)更高的成本;它的內(nèi)存是通過調(diào)用本地操作系統(tǒng)方面的代碼分配的。?
o 視圖緩沖區(qū)?
視圖緩沖區(qū)和緩沖區(qū)復(fù)制很像,不同的只是數(shù)據(jù)類型,所以字節(jié)對應(yīng)關(guān)系也略有不同。比如ByteBuffer.asCharBuffer,那么轉(zhuǎn)換后的緩沖區(qū)通過get操作獲得的元素對應(yīng)備份存儲中的2個字節(jié)。?
o 如何存取無符號整數(shù)??
Java中并沒有直接提供無符號數(shù)值的支持,每個從緩沖區(qū)讀出的無符號值被升到比它大的下一個數(shù)據(jù)類型中。
通道
通道用于在緩沖區(qū)和位于通道另一側(cè)的實(shí)體(文件、套接字)之間有效的傳輸數(shù)據(jù)。相對于緩沖區(qū),通道的“家譜”略顯復(fù)雜:
? 使用通道?
打開通道比較簡單,除了FileChannel,都用open方法打開。?
我們知道,通道是和緩沖區(qū)交互的,從緩沖區(qū)獲取數(shù)據(jù)進(jìn)行傳輸,或?qū)?shù)據(jù)傳輸給緩沖區(qū)。從類繼承層次結(jié)構(gòu)可以看出,通道一般都是雙向的(除FileChannel)。?
下面來看一下通道間數(shù)據(jù)傳輸?shù)拇a:
? 關(guān)閉通道?
通道不能被重復(fù)使用,這點(diǎn)與緩沖區(qū)不同;關(guān)閉通道后,通道將不再連接任何東西,任何的讀或?qū)懖僮鞫紩?dǎo)致ClosedChannelException。?
調(diào)用通道的close()方法時,可能會導(dǎo)致線程暫時阻塞,就算通道處于非阻塞模式也不例外。如果通道實(shí)現(xiàn)了InterruptibleChannel接 口,那么阻塞在該通道上的一個線程被中斷時,該通道將被關(guān)閉,被阻塞線程也會拋出ClosedByInterruptException異常。當(dāng)一個通道 關(guān)閉時,休眠在該通道上的所有線程都將被喚醒并收到一個AsynchronousCloseException異常。?
? 發(fā)散、聚集?
發(fā)散、聚集,又被稱為矢量I/O,簡單而強(qiáng)大的概念,它是指在多個緩沖區(qū)上實(shí)現(xiàn)一個簡單的I/O操作。它減少或避免了緩沖區(qū)的拷貝和系統(tǒng)調(diào)用,它應(yīng)該使用直接緩沖區(qū)以從本地I/O獲取最大性能優(yōu)勢。?
? 文件通道?
? Socket通道?
Socket通道有三個,分別是ServerSocketChannel、SocketChannel和DatagramChannel,而它們又分別對 應(yīng)java.net包中的Socket對象ServerSocket、Socket和DatagramSocket;Socket通道被實(shí)例化時,都會創(chuàng) 建一個對等的Socket對象。?
Socket通道可以運(yùn)行非阻塞模式并且是可選擇的,非阻塞I/O與可選擇性是緊密相連的,這也正是管理阻塞的API要在 SelectableChannel中定義的原因。設(shè)置非阻塞非常簡單,只要調(diào)用configureBlocking(false)方法即可。如果需要中 途更改阻塞模式,那么必須首先獲得blockingLock()方法返回的對象的鎖。?
o ServerSocketChannel?
ServerSocketChannel是一個基于通道的socket監(jiān)聽器。但它沒有bind()方法,因此需要取出對等的Socket對象并使用它來 綁定到某一端口以開始監(jiān)聽連接。在非阻塞模式下,當(dāng)沒有傳入連接在等待時,其accept()方法會立即返回null。正是這種檢查連接而不阻塞的能力實(shí) 現(xiàn)了可伸縮性并降低了復(fù)雜性,選擇性也因此得以實(shí)現(xiàn)。
o SocketChannel?
相對于ServerSocketChannel,它扮演客戶端,發(fā)起到監(jiān)聽服務(wù)器的連接,連接成功后,開始接收數(shù)據(jù)。?
要注意的是,調(diào)用它的open()方法僅僅是打開但并未連接,要建立連接需要緊接著調(diào)用connect()方法;也可以兩步合為一步,調(diào)用open(SocketAddress remote)方法。?
你會發(fā)現(xiàn)connect()方法并未提供timout參數(shù),作為替代方案,你可以用isConnected()、isConnectPending()或finishConnect()方法來檢查連接狀態(tài)。?
o DatagramChannel?
不同于前面兩個通道對象,它是無連接的,它既可以作為服務(wù)器,也可以作為客戶端。?
選擇器
選擇器提供選擇執(zhí)行已經(jīng)就緒的任務(wù)的能力,這使得多元I/O成為可能。就緒選擇和多元執(zhí)行使得單線程能夠有效率地同時管理多個I/O通道。選擇器可謂NIO中的重頭戲,I/O復(fù)用的核心,下面我們來看看這個神奇的東東。
? 基礎(chǔ)概念?
我們先來看下選擇器相關(guān)類的關(guān)系圖:
由圖中可以看出,選擇器類Selector并沒有和通道有直接的關(guān)系,而是通過叫選擇鍵的對象SelectionKey來聯(lián)系的。選擇鍵代表了通道與選擇 器之間的一種注冊關(guān)系,channel()和selector()方法分別返回注冊的通道與選擇器。由類圖也可以看出,一個通道可以注冊到多個選擇器;注 冊方法register()是放在通道類里,而我感覺放在選擇器類里合適點(diǎn)。?
非阻塞特性與多元執(zhí)行的關(guān)系非常密切,如果在阻塞模式下注冊一個通道,系統(tǒng)會拋出IllegalBlockingModeException異常。?
那么,通道注冊到選擇器后,選擇器又是如何實(shí)現(xiàn)就緒選擇的呢?真正的就緒操作是由操作系統(tǒng)來做的,操作系統(tǒng)處理I/O請求并通知各個線程它們的數(shù)據(jù)已經(jīng)準(zhǔn)備好了,而選擇器類提供了這種抽象。?
選擇鍵作為通道與選擇器的注冊關(guān)系,需要維護(hù)這個注冊關(guān)系所關(guān)心的通道操作interestOps()以及通道已經(jīng)準(zhǔn)備好的操作readyOps(),這 兩個方法的返回值都是比特掩碼,另外ready集合是interest集合的子集。選擇鍵類中定義了4種可選擇操作:read、write、 connect和accept。類圖中你可以看到每個可選擇通道都有一個validOps()的抽象方法,每個具體通道各自有不同的有效的可選擇操作集 合,比如ServerSocketChannel的有效操作集合是accept,而SocketChannel的有效操作集合是read、write和 connect。?
回過頭來再看下注冊方法,其第二個參數(shù)是一個比特掩碼,這個參數(shù)就是上面講的這個注冊關(guān)系所關(guān)心的通道操作。在選擇過程中,所關(guān)心的通道操作可以由方法 interestOps(int operations)進(jìn)行修改,但不影響此次選擇過程(在下一次選擇過程中生效)。?
? 使用選擇器?
o 選擇過程?
類圖中可以看出,選擇器類中維護(hù)著兩個鍵的集合:已注冊的鍵的集合keys()和已選擇的鍵的集合selectedKeys(),已選擇的鍵的集合是已注 冊的鍵的集合的子集。已選擇的鍵的集合中的每個成員都被選擇器(在前一個選擇操作中)判斷為已經(jīng)準(zhǔn)備好(所關(guān)心的操作集合中至少一個操作)。 除此之外,其實(shí)選擇器內(nèi)部還維護(hù)著一個已取消的鍵的集合,這個集合包含了cancel()方法被調(diào)用過的鍵。?
選擇器類的核心是選擇過程,基本上來說是對select()、poll()等系統(tǒng)調(diào)用的一個包裝。那么,選擇過程的具體細(xì)節(jié)或步驟是怎樣的呢??
當(dāng)選擇器類的選擇操作select()被調(diào)用時,下面的步驟將被執(zhí)行:?
1.已被取消的鍵的集合被檢查。如果非空,那么該集合中的鍵將從另外兩個集合中移除,并且相關(guān)通道將被注銷。這個步驟結(jié)束后,已取消的鍵的集合將為空。?
2.已注冊的鍵的集合中的鍵的interest集合將被檢查。在這個步驟執(zhí)行過后,對interset集合的改動不會影響剩余的檢查過程。一旦就緒條件被 確定下來,操作系統(tǒng)將會進(jìn)行查詢,以確定每個通道所關(guān)心的操作的真實(shí)就緒狀態(tài)。這可能會阻塞一段時間,最終每個通道的就緒狀態(tài)將確定下來。那些還沒有準(zhǔn)備 好的通道將不會執(zhí)行任何操作;而對于那些操作系統(tǒng)指示至少已經(jīng)準(zhǔn)備好interest集合中的一個操作的通道,將執(zhí)行以下兩種操作中的一種:?
a.如果通道的鍵還沒有在已選擇的鍵的集合中,那么鍵的ready集合將被清空,然后表示操作系統(tǒng)發(fā)現(xiàn)的當(dāng)前通道已經(jīng)準(zhǔn)備好的操作的比特掩碼將被設(shè)置。?
b.如果通道的鍵已處于已選擇的鍵的集合中,鍵的ready集合將被表示操作系統(tǒng)發(fā)現(xiàn)的當(dāng)前通道已經(jīng)準(zhǔn)備好的操作的比特掩碼所更新,所有之前的已經(jīng)不再是就緒狀態(tài)的操作不會被清除。?
3.步驟2可能會花費(fèi)很長時間,特別是調(diào)用的線程處于休眠狀態(tài)。同時,與選擇器相關(guān)的鍵可能會被取消。當(dāng)步驟2結(jié)束時,步驟1將重新執(zhí)行,以完成任意一個在選擇過程中,鍵已經(jīng)被取消的通道的注銷。?
4.select操作返回的值是ready集合在步驟2中被修改的鍵的數(shù)量,而不是已選擇鍵的集合中的通道總數(shù)。返回值不是已經(jīng)準(zhǔn)備好的通道的總數(shù),而是 從上一個select調(diào)用之后進(jìn)入就緒狀態(tài)的通道的數(shù)量。之前調(diào)用中就緒的,并且在本次調(diào)用中仍然就緒的通道不會被計(jì)入。?
o 停止選擇過程
選擇器類提供了方法wakeup(),可以使線程從被阻塞的select()方法中優(yōu)雅的退出,它將選擇器上的第一個還沒有返回的選擇操作立即返回。?
調(diào)用選擇器類的close()方法,那么任何一個阻塞在選擇過程中的線程將被喚醒,與選擇器相關(guān)的通道將被注銷,而鍵將被取消。?
另外,選擇器類也能捕獲InterruptedException異常并調(diào)用wakeup()方法。?
o 并發(fā)性?
? 選擇過程的可擴(kuò)展性?
在單cpu中使用一個線程為多個通道提供服務(wù)可能是個好主意,但對于多cpu的系統(tǒng),單線程必然比多線程在性能上要差很多。?
一個比較不錯的多線程策略是,以所有的通道使用一個選擇器(或多個選擇器,視情況),并將以就緒通道的服務(wù)委托給其他線程。用一個線程監(jiān)控通道的就緒狀態(tài),并使用一個工作線程池來處理接收到的數(shù)據(jù)。講了這么多,下面來看一段用NIO寫的簡單服務(wù)器代碼:
I/O多路復(fù)用模式
I/O多路復(fù)用有兩種經(jīng)典模式:基于同步I/O的reactor和基于異步I/O的proactor。
? Reactor?
o 某個事件處理者宣稱它對某個socket上的讀事件很感興趣;?
o 事件分離者等著這個事件的發(fā)生;?
o 當(dāng)事件發(fā)生了,事件分離器被喚醒,這負(fù)責(zé)通知先前那個事件處理者;?
o 事件處理者收到消息,于是去那個socket上讀數(shù)據(jù)了. 如果需要,它再次宣稱對這個socket上的讀事件感興趣,一直重復(fù)上面的步驟;?
? Proactor?
o 事件處理者直接投遞發(fā)一個寫操作(當(dāng)然,操作系統(tǒng)必須支持這個異步操作). 這個時候,事件處理者根本不關(guān)心讀事件,它只管發(fā)這么個請求,它魂?duì)繅艨M的是這個寫操作的完成事件。這個處理者很拽,發(fā)個命令就不管具體的事情了,只等著別人(系統(tǒng))幫他搞定的時候給他回個話。?
o 事件分離者等著這個讀事件的完成(比較下與Reactor的不同);?
o 當(dāng)事件分離者默默等待完成事情到來的同時,操作系統(tǒng)已經(jīng)在一邊開始干活了,它從目標(biāo)讀取數(shù)據(jù),放入用戶提供的緩存區(qū)中,最后通知事件分離者,這個事情我搞完了;?
o 事件分享者通知之前的事件處理者: 你吩咐的事情搞定了;?
o 事件處理者這時會發(fā)現(xiàn)想要讀的數(shù)據(jù)已經(jīng)乖乖地放在他提供的緩存區(qū)中,想怎么處理都行了。如果有需要,事件處理者還像之前一樣發(fā)起另外一個寫操作,和上面的幾個步驟一樣。?
異步的proactor固然不錯,但它局限于操作系統(tǒng)(要支持異步操作),為了開發(fā)真正獨(dú)立平臺的通用接口,我們可以通過reactor模擬來實(shí)現(xiàn)proactor。
? Proactor(模擬)?
o 等待事件 (Proactor 的工作)?
o 讀數(shù)據(jù)(看,這里變成成了讓 Proactor 做這個事情)?
o 把數(shù)據(jù)已經(jīng)準(zhǔn)備好的消息給用戶處理函數(shù),即事件處理者(Proactor 要做的)?
o 處理數(shù)據(jù) (用戶代碼要做的)?
總結(jié)
本文介紹了 I/O的一些基礎(chǔ)概念及5種I/O模型,NIO是5種模型中的I/O復(fù)用模型;接著進(jìn)入主題Java NIO,分別講了NIO中三個最重要的概念:緩沖區(qū)、通道、選擇器;我們也明白了NIO是如何實(shí)現(xiàn)I/O復(fù)用模型的。最后討論了I/O多路復(fù)用模式中的兩 種模式:reactor和proactor,以及如何用reactor模擬proactor。
參考資料
O'Reilly Java NIO?
Richard Stevens《UNIX網(wǎng)絡(luò)編程 卷1:套接字聯(lián)網(wǎng)API》?
兩種高性能I/O設(shè)計(jì)模式(Reactor/Proactor)的比較?
Understanding Network I/O?
Understanding Disk I/O - when should you be worried?
?
from:?https://yq.aliyun.com/articles/2371
總結(jié)
以上是生活随笔為你收集整理的理解Java NIO的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 攻破JAVA NIO技术壁垒
- 下一篇: JAVA NIO学习一:NIO简介、NI