前言:
傳統(tǒng)的 IO 流還是有很多缺陷的桶现,尤其它的阻塞性加上磁盤讀寫本來就慢蚕冬,會(huì)導(dǎo)致 CPU 使用效率大大降低橘洞。
所以夹孔,jdk 1.4 發(fā)布了 NIO 包陵霉,NIO 的文件讀寫設(shè)計(jì)顛覆了傳統(tǒng) IO 的設(shè)計(jì)琅轧,采用通道+緩存區(qū)使得新式的 IO 操作直接面向緩存區(qū),并且是非阻塞的踊挠,對(duì)于效率的提升真不是一點(diǎn)兩點(diǎn)乍桂,我們一起來看看。
通道 Channel
我們說過效床,NIO 的核心就是通道和緩存區(qū)睹酌,所以它們的工作模式是這樣的:
通道有點(diǎn)類似 IO 中的流,但不同的是剩檀,同一個(gè)通道既允許讀也允許寫憋沿,而任意一個(gè)流要么是讀流要么是寫流。
但是你要明白一點(diǎn)沪猴,通道和流一樣都是需要基于物理文件的辐啄,而每個(gè)流或者通道都通過文件指針操作文件,這里說的「通道是雙向的」也是有前提的运嗜,那就是通道基于隨機(jī)訪問文件『RandomAccessFile』的可讀可寫文件指針壶辜。
『RandomAccessFile』是既可讀又可寫的,所以基于它的通道是雙向的担租,所以砸民,「通道是雙向的」這句話是有前提的,不能斷章取義。
基本的通道類型有如下一些:
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel
FileChannel 是基于文件的通道岭参,SocketChannel 和 ServerSocketChannel 用于網(wǎng)絡(luò) TCP 套接字?jǐn)?shù)據(jù)報(bào)讀寫便贵,DatagramChannel 是用于網(wǎng)絡(luò) UDP 套接字?jǐn)?shù)據(jù)報(bào)讀寫。
通道不能單獨(dú)存在冗荸,它永遠(yuǎn)需要綁定一個(gè)緩存區(qū)承璃,所有的數(shù)據(jù)只會(huì)存在于緩存區(qū)中,無論你是寫或是讀蚌本,必然是緩存區(qū)通過通道到達(dá)磁盤文件盔粹,或是磁盤文件通過通道到達(dá)緩存區(qū)。
即緩存區(qū)是數(shù)據(jù)的「起點(diǎn)」程癌,也是「終點(diǎn)」舷嗡,具體這些通道到底有哪些不同以及該如何使用,基本實(shí)現(xiàn)如何嵌莉,我們介紹完『緩存區(qū)』概念后进萄,再做詳細(xì)學(xué)習(xí)。
緩存區(qū) Buffer
Buffer 是所有具體緩存區(qū)的基類锐峭,是一個(gè)抽象類中鼠,它的實(shí)現(xiàn)類有很多,包含各種類型數(shù)據(jù)的緩存沿癞。
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
MappedByteBuffer
我們以 ByteBuffer 為例進(jìn)行學(xué)習(xí)援雇,其余的緩存區(qū)也都是基于字節(jié)緩存區(qū)的,只不過多了一步字節(jié)轉(zhuǎn)換過程而已椎扬,MappedByteBuffer 是一個(gè)特殊的緩存方式惫搏,我們會(huì)單獨(dú)介紹。
Buffer 中有幾個(gè)重要的成員屬性蚕涤,我們了解一下:
mark 屬性我們已經(jīng)不陌生了筐赔,用于重復(fù)讀。capacity 描述緩存區(qū)容量揖铜,即整個(gè)緩存區(qū)最大能存儲(chǔ)多少數(shù)據(jù)量茴丰。address 用于操作直接內(nèi)存,區(qū)別于 jvm 內(nèi)存蛮位,這一點(diǎn)待會(huì)說明较沪。
而 position 和 limit 我想用一張圖結(jié)合解釋:
由于緩存區(qū)是讀寫共存的鳞绕,所以不同的模式下失仁,這兩個(gè)變量的值也具有不同的意義。
寫模式下们何,所謂寫模式就是將緩存區(qū)中的內(nèi)容寫入通道萄焦。position 代表下一個(gè)字節(jié)應(yīng)該被寫出去的字節(jié)在緩存區(qū)中的位置,limit 表示最后一個(gè)待寫字節(jié)在緩存區(qū)的位置。
讀模式下拂封,所謂讀模式就是從通道讀取數(shù)據(jù)到緩存區(qū)茬射。position 代表下一個(gè)讀出來的字節(jié)應(yīng)當(dāng)存儲(chǔ)在緩存區(qū)的位置,limit 等于 capacity冒签。
相關(guān)的讀寫操作細(xì)節(jié)在抛,待會(huì)會(huì)和大家一起看源碼,以加深對(duì)通道和緩存區(qū)協(xié)作工作的原理萧恕,這里我們先討論一個(gè)大家可能沒怎么關(guān)注過的一個(gè)問題刚梭。
JVM 內(nèi)存劃分為棧和堆,這是大家深入腦海的知識(shí)票唆,但是其實(shí)劃分給 JVM 的還有一塊堆外內(nèi)存朴读,也就是直接內(nèi)存,很多人不知道這塊內(nèi)存是干什么用的走趋。
這是一塊物理內(nèi)存衅金,專門用于 JVM 和 IO 設(shè)備打交道,Java 底層使用 C 語言的 API 調(diào)用操作系統(tǒng)與 IO 設(shè)備進(jìn)行交互簿煌。
例如氮唯,Java 內(nèi)存中有一個(gè)字節(jié)數(shù)組,現(xiàn)在調(diào)用流將它寫入磁盤文件姨伟,那么 JVM 首先會(huì)將這個(gè)字節(jié)數(shù)組先拷貝一份到堆外內(nèi)存中您觉,然后調(diào)用 C 語言 API 指明將某個(gè)連續(xù)地址范圍的數(shù)據(jù)寫入磁盤。
讀操作也是類似授滓,而 JVM 額外做的拷貝工作也是有意義的琳水,因?yàn)?JVM 是基于自動(dòng)垃圾回收機(jī)制運(yùn)行的,所有內(nèi)存中的數(shù)據(jù)會(huì)在 GC 時(shí)不停的被移動(dòng)般堆,如果你調(diào)用系統(tǒng) API 告訴操作系統(tǒng)將內(nèi)存某某位置的內(nèi)存寫入磁盤在孝,而此時(shí)發(fā)生 GC 移動(dòng)了該部分?jǐn)?shù)據(jù),GC 結(jié)束后操作系統(tǒng)是不是就寫錯(cuò)數(shù)據(jù)了淮摔。
所以私沮,JVM 對(duì)于與外圍 IO 設(shè)備交互的情況下,都會(huì)將內(nèi)存數(shù)據(jù)復(fù)制一份到堆外內(nèi)存中和橙,然后調(diào)用系統(tǒng) API 間接的寫入磁盤仔燕,讀也是類似的。由于堆外內(nèi)存不受 GC 管理魔招,所以用完一定得記得釋放晰搀。
理解這一個(gè)小知識(shí)是看懂源碼實(shí)現(xiàn)的前提,不然你可能不知道代碼實(shí)現(xiàn)者在做什么办斑。好了外恕,那我們就先來看看讀操作的基本使用與源碼實(shí)現(xiàn)杆逗。
我們看這么一段代碼,這段代碼我大致分成了四個(gè)部分鳞疲,第一部分用于獲取文件通道罪郊,第二部分用于分配緩存區(qū)并完成讀操作,第三部分用于將緩存區(qū)中數(shù)據(jù)進(jìn)行打印尚洽,第四部分為關(guān)閉通道連接悔橄。
第一部分:
getChannel 方法用于獲取一個(gè)文件相關(guān)的通道實(shí)例,具體實(shí)現(xiàn)如下:
getChannel 方法會(huì)調(diào)用 FileChannelImpl 的工廠方法構(gòu)建一個(gè) FileChannelImpl 實(shí)例腺毫,F(xiàn)ileChannelImpl 是抽象類 FileChannel 的一個(gè)子類實(shí)現(xiàn)橄维。
構(gòu)成 FileChannelImpl 實(shí)例所需的必要參數(shù)有,該文件的文件指針拴曲,該文件的完整路徑争舞,讀寫權(quán)限等。
第二部分:
Buffer 的基本結(jié)構(gòu)我們上述已經(jīng)簡單介紹了澈灼,這里不再贅述了竞川,所謂的緩存區(qū),本質(zhì)上就是字節(jié)數(shù)組叁熔。
ByteBuffer 實(shí)例的構(gòu)建是通過工廠模式產(chǎn)生的委乌,必須指定參數(shù) capacity 作為內(nèi)部字節(jié)數(shù)組的容量。HeapByteBuffer 是虛擬機(jī)的堆上內(nèi)存荣回,所有數(shù)據(jù)都將存儲(chǔ)在堆空間遭贸,我們不久將會(huì)介紹它的一個(gè)兄弟,DirectByteBuffer心软,它被分配在堆外內(nèi)存中壕吹,具體的一會(huì)說。
這個(gè) HeapByteBuffer 的構(gòu)造情況我們不妨跟進(jìn)去看看:
調(diào)用父類的構(gòu)造方法删铃,初始化我們?cè)?ByteBuffer 中提過的一些屬性值耳贬,如 position,capacity猎唁,mark咒劲,limit,offset 以及字節(jié)數(shù)組 hb诫隅。
接著腐魂,我們看看這個(gè) read 方法的調(diào)用鏈。
這個(gè) read 方法是子類 FileChannelImpl 對(duì)父類 FileChannel read 方法的重寫逐纬。這個(gè)方法不是讀操作的核心蛔屹,我們簡單概括一下,該方法首先會(huì)拿到當(dāng)前通道實(shí)例的鎖风题,如果沒有被其他線程占有判导,那么占有該鎖,并調(diào)用 IOUtil 的 read 方法沛硅。
IOUtil 的 read 方法內(nèi)部也調(diào)用了很多方法眼刃,有的甚至是本地方法,這里只簡單介紹一下整個(gè) read 方法的大體邏輯摇肌,具體細(xì)節(jié)留待大家自行學(xué)習(xí)擂红。
首先判斷我們的 ByteBuffer 實(shí)例是不是一個(gè) DirectBuffer,也就是判斷當(dāng)前的 ByteBuffer 實(shí)例是不是被分配在直接內(nèi)存中围小,如果是昵骤,那么將調(diào)用readIntoNativeBuffer 方法從磁盤讀取數(shù)據(jù)直接放入 ByteBuffer 實(shí)例所在的直接內(nèi)存中。
否則肯适,虛擬機(jī)將在直接內(nèi)存區(qū)域分配一塊內(nèi)存变秦,該內(nèi)存區(qū)域的首地址存儲(chǔ)在 var5 實(shí)例的 address 屬性中。
接著從磁盤讀取數(shù)據(jù)放入 var5 所代表的直接內(nèi)存區(qū)域中框舔。
最后蹦玫,put 方法會(huì)將 var5 所代表的直接內(nèi)存區(qū)域中的數(shù)據(jù)寫入到 var1 所代表的堆內(nèi)緩存區(qū)并釋放臨時(shí)創(chuàng)建的直接內(nèi)存空間。
這樣刘绣,我們傳入的緩存區(qū)中就成功的被讀入了數(shù)據(jù)樱溉。寫操作是相反的,大家可以自行類比纬凤,反正堆內(nèi)數(shù)據(jù)想要到達(dá)磁盤就必定要經(jīng)過堆外內(nèi)存的復(fù)制過程福贞。
第三第四部分比較簡單,這里不再贅述了停士。提醒一下挖帘,想要更好的使用這個(gè)通道和緩存區(qū)進(jìn)行文件讀寫操作,你就一定得對(duì)緩存區(qū)的幾個(gè)變量的值時(shí)刻把握住恋技,position 和 limit 當(dāng)前的值是什么肠套,大致什么位置,一定得清晰猖任,否則這個(gè)讀寫共存的緩存區(qū)可能會(huì)讓你暈頭轉(zhuǎn)向你稚。
選擇器 Selector
Selector 是 Java NIO 的一個(gè)組件,它用于監(jiān)聽多個(gè) Channel 的各種狀態(tài)朱躺,用于管理多個(gè) Channel刁赖。但本質(zhì)上由于 FileChannel 不支持注冊(cè)選擇器,所以 Selector 一般被認(rèn)為是服務(wù)于網(wǎng)絡(luò)套接字通道的长搀。
而大家口中的「NIO 是非阻塞的」宇弛,準(zhǔn)確來說,指的是網(wǎng)絡(luò)編程中客戶端與服務(wù)端連接交換數(shù)據(jù)的過程是非阻塞的源请。普通的文件讀寫依然是阻塞的枪芒,和 IO 是一樣的彻况,這一點(diǎn)可能很多初學(xué)者會(huì)懵,包括我當(dāng)時(shí)也總想不通為什么說 NIO 的文件讀寫是非阻塞的舅踪,明明就是阻塞的纽甘。
創(chuàng)建一個(gè)選擇器一般是通過 Selector 的工廠方法,Selector.open :
而一個(gè)通道想要注冊(cè)到某個(gè)選擇器中抽碌,必須調(diào)整模式為非阻塞模式悍赢,例如:
以上代碼是注冊(cè)一個(gè)通道到選擇器中的最簡單版本,支持注冊(cè)選擇器的通道都有一個(gè) register 方法货徙,該方法就是用于注冊(cè)當(dāng)前實(shí)例通道到指定選擇器的左权。
該方法的第一個(gè)參數(shù)就是目標(biāo)選擇器,第二個(gè)參數(shù)其實(shí)是一個(gè)二進(jìn)制掩碼痴颊,它指明當(dāng)前選擇器感興趣當(dāng)前通道的哪些事件赏迟。以枚舉類型提供了以下幾種取值:
int OP_READ = 1 << 0;
int OP_WRITE = 1 << 2;
int OP_CONNECT = 1 << 3;
int OP_ACCEPT = 1 << 4;
這種用二進(jìn)制掩碼來表示某些狀態(tài)的機(jī)制,我們?cè)谥v述虛擬機(jī)類類文件結(jié)構(gòu)的時(shí)候也遇到過蠢棱,它就是用一個(gè)二進(jìn)制位來描述一種狀態(tài)瀑梗。
register 方法會(huì)返回一個(gè) SelectionKey 實(shí)例,該實(shí)例代表的就是選擇器與通道的一個(gè)關(guān)聯(lián)關(guān)系裳扯。你可以調(diào)用它的 selector 方法返回當(dāng)前相關(guān)聯(lián)的選擇器實(shí)例抛丽,也可以調(diào)用它的 channel 方法返回當(dāng)前關(guān)聯(lián)關(guān)系中的通道實(shí)例。
除此之外饰豺,SelectionKey 的 readyOps 方法將返回當(dāng)前選擇感興趣當(dāng)前通道中事件中準(zhǔn)備就緒的事件集合亿鲜,依然返回的一個(gè)整型數(shù)值,也就是一個(gè)二進(jìn)制掩碼冤吨。
例如:
假如 readySet 的值為 13蒿柳,二進(jìn)制 「0000 1101」,從后向前數(shù)漩蟆,第一位為 1垒探,第三位為 1,第四位為 1怠李,那么說明選擇器關(guān)聯(lián)的通道圾叼,讀就緒、寫就緒捺癞,連接就緒夷蚊。
所以,當(dāng)我們注冊(cè)一個(gè)通道到選擇器之后髓介,就可以通過返回的 SelectionKey 實(shí)例監(jiān)聽該通道的各種事件惕鼓。
當(dāng)然,一旦某個(gè)選擇器中注冊(cè)了多個(gè)通道唐础,我們不可能一個(gè)一個(gè)的記錄它們注冊(cè)時(shí)返回的 SelectionKey 實(shí)例來監(jiān)聽通道事件箱歧,選擇器應(yīng)當(dāng)有方法返回所有注冊(cè)成功的通道相關(guān)的 SelectionKey 實(shí)例矾飞。
selectedKeys 方法會(huì)返回選擇器中注冊(cè)成功的所有通道的 SelectionKey 實(shí)例集合。我們通過這個(gè)集合的 SelectionKey 實(shí)例呀邢,可以得到所有通道的事件就緒情況并進(jìn)行相應(yīng)的處理操作洒沦。
下面我們以一個(gè)簡單的客戶端服務(wù)端連接通訊的實(shí)例應(yīng)用一下上述理論知識(shí):
服務(wù)端代碼:
這段小程序的運(yùn)行的實(shí)際效果是這樣的,客戶端建立請(qǐng)求到服務(wù)端驼鹅,待請(qǐng)求完全建立微谓,客戶端會(huì)去檢查服務(wù)端是否有數(shù)據(jù)寫回森篷,而服務(wù)端的任務(wù)就很簡單了输钩,接受任意客戶端的請(qǐng)求連接并為它寫回一段數(shù)據(jù)。
別看整個(gè)過程很簡單仲智,但只要你有一點(diǎn)模糊的地方买乃,你這個(gè)功能就不可能實(shí)現(xiàn),不信你試試钓辆,尤其是加了選擇器的客戶端代碼剪验,更值得大家一行一行分析。提醒一點(diǎn)的是前联,大家應(yīng)更多的關(guān)注于哪些方法是阻塞的功戚,哪些是非阻塞的,這會(huì)有助于分析代碼似嗤。
這其實(shí)也算一個(gè)最最簡單的服務(wù)器客戶端請(qǐng)求模型了啸臀,理解了這一點(diǎn)相信會(huì)有助于理解瀏覽器與 Web 服務(wù)器的工作原理的,這里我就不再帶大家分析了烁落,有任何不同看法的也歡迎給我留言乘粒,咱們一起學(xué)習(xí)探討。
想必你也能發(fā)現(xiàn)伤塌,加了選擇器的代碼會(huì)復(fù)雜很多灯萍,也并不一定高效于原來的代碼,這其實(shí)是因?yàn)槟愕墓δ鼙容^簡單每聪,并不涉及大量通道處理旦棉,邏輯一旦復(fù)雜起來,選擇器給你帶來的好處會(huì)非常明顯药薯。
其實(shí)他爸,NIO 中還有一塊 AIO ,也就是異步 IO 并沒有介紹果善,因?yàn)楫惒?IO 涉及到很多其他方面知識(shí)诊笤,這里暫時(shí)不做介紹,后續(xù)文章將單獨(dú)介紹異步任務(wù)等相關(guān)內(nèi)容巾陕。
如果大家想學(xué)習(xí)以下路線內(nèi)容讨跟,在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流群纪他。交流學(xué)習(xí)群號(hào):478030634 里面會(huì)分享一些資深架構(gòu)師錄制的視頻錄像:有Spring,MyBatis晾匠,Netty源碼分析茶袒,高并發(fā)、高性能凉馆、分布式薪寓、微服務(wù)架構(gòu)的原理,JVM性能優(yōu)化澜共、分布式架構(gòu)等這些成為架構(gòu)師必備的知識(shí)體系向叉。還能領(lǐng)取免費(fèi)的學(xué)習(xí)資源,目前受益良多
注:關(guān)注作者微信公眾號(hào)嗦董,了解更多分布式架構(gòu)母谎、微服務(wù)、netty京革、MySQL奇唤、spring、匹摇、性能優(yōu)化咬扇、等知識(shí)點(diǎn)。公眾號(hào):《Java爛豬皮》