柚子快報(bào)邀請(qǐng)碼778899分享:Netty01-NIO與BIO
柚子快報(bào)邀請(qǐng)碼778899分享:Netty01-NIO與BIO
NIO
什么是NIO
Java NIO 全稱 java non-blocking IO,是指JDK 1.4 及以上版本提供的新API(New IO)。從 JDK1.4 開始,Java 提供了一系列改進(jìn)的輸入/輸出的新特性,為所有的原始類型(boolean類型除外)提供緩存支持的數(shù)據(jù)容器,使用它可以提供同步非阻塞式的高伸縮性網(wǎng)絡(luò)。
? NIO的面向緩沖區(qū)和非阻塞I/O模式確實(shí)提供了高靈活性和高性能的優(yōu)勢(shì),使得一個(gè)線程可以處理多個(gè)操作,從而提高了系統(tǒng)的可伸縮性。HTTP/2.0利用了多路復(fù)用技術(shù),實(shí)現(xiàn)了在同一個(gè)連接上并發(fā)處理多個(gè)請(qǐng)求,相比HTTP/1.1大幅提升了并發(fā)請(qǐng)求的效率 .
NIO的三大組件(Channel 通道 , Buffer 緩沖區(qū) ,selector 選擇器
selector 的作用就是配合一個(gè)線程來管理多個(gè) channel,獲取這些 channel 上發(fā)生的事件,這些 channel 工作在非阻塞模式下,當(dāng)一個(gè)channel中沒有執(zhí)行任務(wù)時(shí),可以去執(zhí)行其他channel中的任務(wù)。適合連接數(shù)多,但流量較少的場(chǎng)景 。
channel
每個(gè)channel會(huì)對(duì)應(yīng)一個(gè)buffer ,每個(gè)Buffer其實(shí)是一個(gè)內(nèi)存塊。
BIO是面向字節(jié)流(stream),與Channel的區(qū)別在于
stream 不會(huì)自動(dòng)緩沖數(shù)據(jù),channel 會(huì)利用系統(tǒng)提供的發(fā)送緩沖區(qū)、接收緩沖區(qū)(更為底層)stream 僅支持阻塞 API,channel 同時(shí)支持阻塞、非阻塞 API,網(wǎng)絡(luò) channel 可配合 selector 實(shí)現(xiàn)多路復(fù)用二者均為全雙工即讀寫可以同時(shí)進(jìn)行雖然Stream是單向流動(dòng)的,但是它也是全雙工的。
buffer
? 緩沖區(qū)本質(zhì)上是一塊可以寫入數(shù)據(jù),然后可以從中讀取數(shù)據(jù)的內(nèi)存。這塊內(nèi)存被包裝 成 NIO Buffer 對(duì)象,并提供了一組方法,用來方便的訪問該塊內(nèi)存。緩沖區(qū)實(shí)際上是 一個(gè)容器對(duì)象,更直接的說,其實(shí)就是一個(gè)數(shù)組,在 NIO 庫中,所有數(shù)據(jù)都是用緩沖 區(qū)處理的。在讀取數(shù)據(jù)時(shí),它是直接讀到緩沖區(qū)中的; 在寫入數(shù)據(jù)時(shí),它也是寫入到 緩沖區(qū)中的;任何時(shí)候訪問 NIO 中的數(shù)據(jù),都是將它放到緩沖區(qū)中。而在面向流 I/O 系統(tǒng)中,所有數(shù)據(jù)都是直接寫入或者直接將數(shù)據(jù)讀取到 Stream 對(duì)象中。
buffer的核心屬性
capacity:緩沖區(qū)的容量。通過構(gòu)造函數(shù)賦予,一旦設(shè)置,無法更改limit:緩沖區(qū)的界限。位于limit 后的數(shù)據(jù)不可讀寫。緩沖區(qū)的限制不能為負(fù),并且不能大于其容量position:下一個(gè)讀寫位置的索引(類似PC)。緩沖區(qū)的位置不能為負(fù),并且不能大于limitmark:記錄當(dāng)前position的值。position被改變后,可以通過調(diào)用reset() 方法恢復(fù)到mark的位置。
以上四個(gè)屬性必須滿足以下要求
mark <= position <= limit <= capacity
buffer的核心方法
put()方法
put()方法可以將一個(gè)數(shù)據(jù)放入到緩沖區(qū)中。進(jìn)行該操作后,postition的值會(huì)+1,指向下一個(gè)可以放入的位置。capacity = limit ,為緩沖區(qū)容量的值
flip()方法
flip()方法會(huì)切換對(duì)緩沖區(qū)的操作模式,由寫->讀 / 讀->寫進(jìn)行該操作后
如果是寫模式->讀模式,position = 0 , limit 指向最后一個(gè)元素的下一個(gè)位置,capacity不變?nèi)绻亲x->寫,則恢復(fù)為put()方法中的值
get()方法
get()方法會(huì)讀取緩沖區(qū)中的一個(gè)值 進(jìn)行該操作后,position會(huì)+1,如果超過了limit則會(huì)拋出異常 注意:get(i)方法不會(huì)改變position的值
一些IO模型
同步:線程自己去獲取結(jié)果(一個(gè)線程)
例如:線程調(diào)用一個(gè)方法后,需要等待方法返回結(jié)果 異步:線程自己不去獲取結(jié)果,而是由其它線程返回結(jié)果(至少兩個(gè)線程)
例如:線程A調(diào)用一個(gè)方法后,繼續(xù)向下運(yùn)行,運(yùn)行結(jié)果由線程B返回
當(dāng)調(diào)用一次 channel.read 或 stream.read 后,會(huì)由用戶態(tài)切換至操作系統(tǒng)內(nèi)核態(tài)來完成真正數(shù)據(jù)讀取,而讀取又分為兩個(gè)階段,分別為:
等待數(shù)據(jù)階段復(fù)制數(shù)據(jù)階段
阻塞IO
用戶線程進(jìn)行read操作時(shí),需要等待操作系統(tǒng)執(zhí)行實(shí)際的read操作,此期間用戶線程是被阻塞的,無法執(zhí)行其他操作
非阻塞IO
用戶線程 在一個(gè)循環(huán)中一直調(diào)用read方法 ,若內(nèi)核空間中還沒有數(shù)據(jù)可讀,立即返回
只是在等待階段非阻塞 用戶線程發(fā)現(xiàn)內(nèi)核空間中有數(shù)據(jù)后,等待內(nèi)核空間執(zhí)行復(fù)制數(shù)據(jù),待復(fù)制結(jié)束后返回結(jié)果
多路復(fù)用
Java中通過Selector實(shí)現(xiàn)多路復(fù)用
當(dāng)沒有事件是,調(diào)用select方法會(huì)被阻塞住一旦有一個(gè)或多個(gè)事件發(fā)生后,就會(huì)處理對(duì)應(yīng)的事件,從而實(shí)現(xiàn)多路復(fù)用
多路復(fù)用與阻塞IO的區(qū)別
阻塞IO模式下,若線程因accept事件被阻塞,發(fā)生read事件后,仍需等待accept事件執(zhí)行完成后,才能去處理read事件多路復(fù)用模式下,一個(gè)事件發(fā)生后,若另一個(gè)事件處于阻塞狀態(tài),不會(huì)影響該事件的執(zhí)行
異步IO
線程1調(diào)用方法后理解返回,不會(huì)被阻塞也不需要立即獲取結(jié)果 當(dāng)方法的運(yùn)行結(jié)果出來以后,由線程2將結(jié)果返回給線程1 零拷貝 零拷貝指的是數(shù)據(jù)無需拷貝到 JVM 內(nèi)存中,同時(shí)具有以下三個(gè)優(yōu)點(diǎn)
更少的用戶態(tài)與內(nèi)核態(tài)的切換不利用 cpu 計(jì)算,減少 cpu 緩存?zhèn)喂蚕砹憧截愡m合小文件傳輸 傳統(tǒng) IO 問題 java 本身并不具備 IO 讀寫能力,因此 read 方法調(diào)用后,要從 Java 程序的用戶態(tài)切換至內(nèi)核態(tài),去調(diào)用操作系統(tǒng)(Kernel)的讀能力,將數(shù)據(jù)讀入內(nèi)核緩沖區(qū)。這期間用戶線程阻塞,操作系統(tǒng)使用 DMA(Direct Memory Access)來實(shí)現(xiàn)文件讀,其間也不會(huì)使用 CPU DMA 也可以理解為硬件單元,用來解放 cpu 完成文件 IO 從內(nèi)核態(tài)切換回用戶態(tài),將數(shù)據(jù)從內(nèi)核緩沖區(qū)讀入用戶緩沖區(qū)(即 byte[] buf),這期間 CPU 會(huì)參與拷貝,無法利用 DMA 調(diào)用 write 方法,這時(shí)將數(shù)據(jù)從用戶緩沖區(qū)(byte[] buf)寫入 socket 緩沖區(qū),CPU 會(huì)參與拷貝 接下來要向網(wǎng)卡寫數(shù)據(jù),這項(xiàng)能力 Java 又不具備,因此又得從用戶態(tài)切換至內(nèi)核態(tài),調(diào)用操作系統(tǒng)的寫能力,使用 DMA 將 socket 緩沖區(qū)的數(shù)據(jù)寫入網(wǎng)卡,不會(huì)使用 CPU
可以看到中間環(huán)節(jié)較多,java 的 IO 實(shí)際不是物理設(shè)備級(jí)別的讀寫,而是緩存的復(fù)制,底層的真正讀寫是操作系統(tǒng)來完成的
用戶態(tài)與內(nèi)核態(tài)的切換發(fā)生了 3 次,這個(gè)操作比較重量級(jí)數(shù)據(jù)拷貝了共 4 次
NIO 優(yōu)化
通過 DirectByteBuf
ByteBuffer.allocate(10)
底層對(duì)應(yīng) HeapByteBuffer,使用的還是 Java 內(nèi)存 ByteBuffer. allocateDirect (10)
底層對(duì)應(yīng)DirectByteBuffer,使用的是操作系統(tǒng)內(nèi)存
大部分步驟與優(yōu)化前相同,唯有一點(diǎn):Java 可以使用 DirectByteBuffer 將堆外內(nèi)存映射到 JVM 內(nèi)存中來直接訪問使用
這塊內(nèi)存不受 JVM 垃圾回收的影響,因此內(nèi)存地址固定,有助于 IO 讀寫Java 中的 DirectByteBuf 對(duì)象僅維護(hù)了此內(nèi)存的虛引用,內(nèi)存回收分成兩步
DirectByteBuffer 對(duì)象被垃圾回收,將虛引用加入引用隊(duì)列
當(dāng)引用的對(duì)象ByteBuffer被垃圾回收以后,虛引用對(duì)象Cleaner就會(huì)被放入引用隊(duì)列中,然后調(diào)用Cleaner的clean方法來釋放直接內(nèi)存DirectByteBuffer 的釋放底層調(diào)用的是 Unsafe 的 freeMemory 方法 通過專門線程訪問引用隊(duì)列,根據(jù)虛引用釋放堆外內(nèi)存 減少了一次數(shù)據(jù)拷貝,用戶態(tài)與內(nèi)核態(tài)的切換次數(shù)沒有減少
進(jìn)一步優(yōu)化1
以下兩種方式都是零拷貝,即無需將數(shù)據(jù)拷貝到用戶緩沖區(qū)中(JVM內(nèi)存中)
底層采用了 linux 2.1 后提供的 sendFile 方法,Java 中對(duì)應(yīng)著兩個(gè) channel 調(diào)用 transferTo/transferFrom 方法拷貝數(shù)據(jù)
Java 調(diào)用 transferTo 方法后,要從 Java 程序的用戶態(tài)切換至內(nèi)核態(tài),使用 DMA將數(shù)據(jù)讀入內(nèi)核緩沖區(qū),不會(huì)使用 CPU數(shù)據(jù)從內(nèi)核緩沖區(qū)傳輸?shù)?socket 緩沖區(qū),CPU 會(huì)參與拷貝最后使用 DMA 將 socket 緩沖區(qū)的數(shù)據(jù)寫入網(wǎng)卡,不會(huì)使用 CPU
這種方法下
只發(fā)生了1次用戶態(tài)與內(nèi)核態(tài)的切換數(shù)據(jù)拷貝了 3 次
進(jìn)一步優(yōu)化2
linux 2.4 對(duì)上述方法再次進(jìn)行了優(yōu)化
Java 調(diào)用 transferTo 方法后,要從 Java 程序的用戶態(tài)切換至內(nèi)核態(tài),使用 DMA將數(shù)據(jù)讀入內(nèi)核緩沖區(qū),不會(huì)使用 CPU只會(huì)將一些 offset 和 length 信息拷入 socket 緩沖區(qū),幾乎無消耗使用 DMA 將 內(nèi)核緩沖區(qū)的數(shù)據(jù)寫入網(wǎng)卡,不會(huì)使用 CPU
整個(gè)過程僅只發(fā)生了1次用戶態(tài)與內(nèi)核態(tài)的切換,數(shù)據(jù)拷貝了 2 次
參考: 黑馬程序員netty Nyima筆記
柚子快報(bào)邀請(qǐng)碼778899分享:Netty01-NIO與BIO
精彩內(nèi)容
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場(chǎng)。
轉(zhuǎn)載請(qǐng)注明,如有侵權(quán),聯(lián)系刪除。