摘要:因?yàn)樗械臄?shù)據(jù)從最底層講是字節(jié),那么就可以使用字節(jié)流這個(gè)概念去指代數(shù)據(jù)動態(tài)轉(zhuǎn)移這個(gè)過程。而數(shù)據(jù)的轉(zhuǎn)移,就是把一堆字節(jié)流從運(yùn)往。創(chuàng)建內(nèi)存中的中轉(zhuǎn)區(qū)域,然后將上面的文件的字節(jié)流直接接入到這個(gè)。然后再從把字節(jié)流輸出到對應(yīng)的。
I/O的很多操作和使用,其實(shí)并不是一個(gè)非常直觀的概念,特別是打開文件、創(chuàng)建buffer。這對于終端用戶來講是個(gè)非常奇葩和奇怪的過程。我只是想要從一個(gè)文件里讀取內(nèi)容,從過程上來講,我只需要知道:
讀取的source文件
寫入的目的地
那我干嘛要去關(guān)心神馬打開文件、創(chuàng)建stream和buffer?!
所以,要理解I/O這一套東西以及它所涉及的stream、buffer,你必須先理解計(jì)算機(jī)的底層是如何工作的。如果沒有這一步的底層基礎(chǔ)理論做支撐,所有的I/O操作將無法變得直觀。
為理解I/O所需要用到的底層知識并不算多,就幾點(diǎn):
計(jì)算機(jī)的對數(shù)據(jù)的操作一定要經(jīng)過內(nèi)存。無論你是計(jì)算出來的數(shù)據(jù)、從硬盤中的文件讀取的數(shù)據(jù)、從網(wǎng)絡(luò)中讀取到的數(shù)據(jù),都必須要經(jīng)過內(nèi)存。source data先到內(nèi)存,然后內(nèi)存再到destination。
既然涉及到內(nèi)存,就存在一個(gè)“有限”的問題。所有的程序都是往內(nèi)存跑,無疑這一個(gè)非常寶貴的資源。那么你的“傳輸數(shù)據(jù)”任務(wù),并沒有那么高的優(yōu)先級可以任意地去占有這個(gè)資源。你有且只能獲取一部分,特別地,還應(yīng)該是相對較小的一部分區(qū)域,來供你做數(shù)據(jù)傳輸。
所有在計(jì)算機(jī)中的數(shù)據(jù),無論是文字、圖片、聲音都是0、1這樣的bits。所以,最為通用的傳輸數(shù)據(jù)方式必然是面向字節(jié)的,也就是圍繞字節(jié)這個(gè)概念來做的。
面對以上這些現(xiàn)實(shí),你不得不在programming時(shí)考慮上述問題。因?yàn)槟悴皇墙K端用戶只需要一個(gè)簡單的接口。你是細(xì)節(jié)的操作者,必須對以上限制做出具體的可操作性的回應(yīng)。
因?yàn)槟阒荒苁褂靡恍〔糠值膬?nèi)存空間做數(shù)據(jù)轉(zhuǎn)移,所以這就必然需要一個(gè)buffer的概念去指代內(nèi)存這部分的小空間。所以你要創(chuàng)建buffer并為它分配大小,然后所有的數(shù)據(jù)轉(zhuǎn)移都通過這塊小小的中轉(zhuǎn)站。(這就像是一個(gè)城市的快遞中心,所有全國各地發(fā)往這個(gè)地區(qū)的快件,都必須通過這個(gè)中轉(zhuǎn)站來做統(tǒng)一調(diào)配。)這是對計(jì)算機(jī)的體系架構(gòu)——所有數(shù)據(jù)都必須通過內(nèi)存——所作出的回應(yīng)。
因?yàn)樗械臄?shù)據(jù)從最底層講是字節(jié)(bit),那么就可以使用字節(jié)流這個(gè)概念去指代數(shù)據(jù)動態(tài)轉(zhuǎn)移這個(gè)過程。而數(shù)據(jù)的轉(zhuǎn)移,就是把一堆字節(jié)流從source運(yùn)往destination。但由于上面的原因,這個(gè)過程無法直接完成,所以你必須把字節(jié)流從:source --> buffer --> destination,或者destination --> buffer --> source。
由于傳輸?shù)亩际亲止?jié)流,所以你需要一個(gè)工具把這個(gè)stream給開墾出來,所以你需要有一個(gè)File式的對象,從上面可以取得一個(gè)Channel或者Stream,也即是把file轉(zhuǎn)換為字節(jié)流的池子,以便直接把文件的字節(jié)流拿給buffer。它們就像是data的礦源,通過buffer這輛采礦的小車,不斷地把礦石(data)從礦源(source)運(yùn)到外面(destination)。
有了這部分的知識,我們再來看“Java中的NIO是如何讀取文件的”就不會變得怪異了。
RandomAccessFile aFile = new RandomAccessFile("test.txt", "rw"); FileChannel inChannel = aFile.getChannel(); //create buffer with capacity of 48 bytes ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf); //read into buffer. while (bytesRead != -1) { buf.flip(); //make buffer ready for read while(buf.hasRemaining()){ System.out.print((char) buf.get()); // read 1 byte at a time } buf.clear(); //make buffer ready for writing bytesRead = inChannel.read(buf); } aFile.close();
讓我們將之前討論的過程復(fù)現(xiàn)一遍:
首先創(chuàng)建RandomAccessFile對象,來提供真實(shí)文件test.txt在Java中對應(yīng)的對象(可以理解為一個(gè)bean)。這個(gè)對象將提供各種服務(wù)來配合Java內(nèi)部各種機(jī)制的操作。無疑,提供一個(gè)Channel是它的本質(zhì)工作之一。
創(chuàng)建內(nèi)存中的中轉(zhuǎn)區(qū)域buffer,然后將上面的文件Channel的字節(jié)流直接接入到這個(gè)buffer。
然后再從buffer把字節(jié)流輸出到System.out.print對應(yīng)的std io。
接下來可以深入更多的細(xì)節(jié)。
由于buffer是被重復(fù)利用的部分,所以這涉及到清理buffer的概念buf.clear()。那你可能會說,為什么不可以自動地清理buffer?因?yàn)檫@里的buffer是一個(gè)底層的基礎(chǔ)服務(wù)。對于上層的應(yīng)用來講,有些場景是需要清理buffer,有些場景是需要復(fù)用buffer的內(nèi)容。你怎么可以一概而論地認(rèn)為所有應(yīng)用場景都是只需要消費(fèi)一次buffer中的數(shù)據(jù)呢?所以,作為底層設(shè)施來講,你必須提供足夠的靈活性,讓developer自己決定是否需要清理buffer。
再來比較奇怪的是flip()這部分,為什么需要對buffer做flip操作呢?這就涉及到內(nèi)存中buffer的管理問題。
這里的管理方式其實(shí)很常見,在內(nèi)存中,基本上都是以數(shù)組的形式提供堆棧結(jié)構(gòu)來管理數(shù)據(jù)。那么,這就涉及到對這個(gè)數(shù)組的操作問題。你要有一個(gè)表征position的指針來指導(dǎo)數(shù)據(jù)的寫入方向。
對“寫數(shù)據(jù)”這個(gè)過程來講,數(shù)據(jù)的position指針是從起始點(diǎn),index為0的點(diǎn),逐步增大index來寫數(shù)據(jù)的。只要一直在做“寫”操作,這個(gè)指針就會在buffer中不斷地往index增大的方向移動。
顯然,當(dāng)你需要讀數(shù)據(jù)時(shí),你是需要從這個(gè)buffer的起始點(diǎn)再開始。所以,你需要一個(gè)操作把position指針復(fù)位到起始位置,然后從這個(gè)地方開始不斷地往下讀。(一個(gè)值得思考的問題是:為什么不引入兩個(gè)position指針,一個(gè)用來讀,一個(gè)用來寫。這樣不是就不用把“讀”的position指針復(fù)位了嗎?)
如果多走一步,為了驗(yàn)證flip()是否真的在讓position指針復(fù)原,你還可以使用以下代碼:
System.out.println("Read " + bytesRead); // switch the buffer from writing mode into reading mode buf.flip(); while (buf.hasRemaining()) { System.out.println((char) buf.get()); } System.out.println("---------------------------"); // reset the pointer back to original point again buf.flip(); while (buf.hasRemaining()) { System.out.println((char) buf.get()); }
可以看到,從buffer中又一次獲取到了同樣的信息。
從這個(gè)例子可以看到很多深層次的東西。例如,為什么你必須了解底層的內(nèi)存運(yùn)作機(jī)制、操作系統(tǒng)的運(yùn)轉(zhuǎn)機(jī)制?因?yàn)檫@些細(xì)節(jié)決定了你該以什么樣的方式去設(shè)計(jì)你的編程模式,也決定了你應(yīng)該如何去理解編程語言中提供的一些機(jī)制,或者為什么一個(gè)庫應(yīng)該會這樣設(shè)計(jì)。這是一切具體行動的現(xiàn)實(shí)。
“具備什么功能”是這底層基礎(chǔ)設(shè)施提供的封裝好的API。但你要做的是programming的工作,不得不利用底層的基礎(chǔ)設(shè)施去構(gòu)建新的服務(wù)和產(chǎn)品。如果沒辦法理解這些底層機(jī)制,你就沒辦法真正地去構(gòu)建東西。很多的問題,其實(shí)可以被繞過又或是不可能被實(shí)現(xiàn),不在于邏輯有問題,而是單純的信息差,你并不知道這個(gè)構(gòu)建出的抽閑概念下面隱藏的真實(shí)東西。
如果能夠理解內(nèi)存的利用方式,那么,“分片、以stack的形式來做操作”的模式將成為你本能的一部分。進(jìn)而,涉及到的position移動或者flip()的指針回調(diào)問題,就會成為你的直觀。
當(dāng)然,積累這部分的基礎(chǔ)知識是非??菰锖头ξ兜?。但如同所有的基本功,它們不會在短期內(nèi)為你提供足夠的回報(bào),但卻會為你將來形成正確的“直覺”和“直觀”做出巨大的貢獻(xiàn)。
任何的抽象概念都具備直觀,只不過這個(gè)直觀所依賴的基礎(chǔ)不同。抽象如“概率論基礎(chǔ)”,其“形象”的直觀,其實(shí)是數(shù)學(xué)系本科所學(xué)的“經(jīng)典概率論”,否則你會迷失在“測度論”的細(xì)節(jié)里。但這個(gè)“直觀”對于其它專業(yè)的人來講,并不直觀,甚至是異常復(fù)雜。而這個(gè)就是所謂的牢固的前提基礎(chǔ)知識。進(jìn)一步,如果你想要學(xué)好“概率論基礎(chǔ)”這樣高度抽象的topic,你必須先夯實(shí)“經(jīng)典概率論”這個(gè)基礎(chǔ),必須先建立對它的深刻認(rèn)知。否則,你對“概率論基礎(chǔ)”的理解根本無從談起。
同樣的,如果你希望理解類似于編程語言中I/O庫的設(shè)計(jì)、理解各類緩存中間件、消息隊(duì)列中間件的設(shè)計(jì),你必須要先建立“計(jì)算機(jī)如何運(yùn)作”這個(gè)前提基礎(chǔ)。只有你熟悉了計(jì)算機(jī)的運(yùn)作方式,能夠以計(jì)算機(jī)底層的習(xí)慣去思考問題、處理問題,你才能夠看到各種組件設(shè)計(jì)的直觀,才會看到各種莫名其妙的“繞路”到底是在解決什么、是為了什么。
所以,這樣一個(gè)學(xué)習(xí)過程是任何抽象技能所避免不了的。你必須通過反復(fù)的閱讀和練習(xí)來掌握第一層的基礎(chǔ)概念,熟悉到讓這一層的抽象變成你腦海中的一個(gè)條件反射式的直觀。再這個(gè)新建立的直觀本能基礎(chǔ)上,你才可以去理解更高層次的抽象。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/72616.html
摘要:為解決這問題,我們發(fā)現(xiàn)元兇處在一線程一請求上,如果一個(gè)線程能同時(shí)處理多個(gè)請求,那么在高并發(fā)下性能上會大大改善。這樣一個(gè)線程可以同時(shí)發(fā)起多個(gè)調(diào)用,并且不需要同步等待數(shù)據(jù)就緒。表示當(dāng)前就緒的事件類型。 JAVA NIO 一步步構(gòu)建I/O多路復(fù)用的請求模型 摘要:本文屬于原創(chuàng),歡迎轉(zhuǎn)載,轉(zhuǎn)載請保留出處:https://github.com/jasonGeng88/blog 文章一:JAVA ...
摘要:操作系統(tǒng)是能夠獲取到事件操作完成的事件,基于回調(diào)函數(shù)機(jī)制和操作系統(tǒng)的操作控制實(shí)現(xiàn)事件檢測機(jī)制。 前面的文章NIO基礎(chǔ)知識介紹了Java NIO的一些基本的類及功能說明,Java NIO是用來替換java 傳統(tǒng)IO的,NIO的一些新的特性在網(wǎng)絡(luò)交互方面會更加的明顯。 Java 傳統(tǒng)IO的弊端 ????基于JVM來實(shí)現(xiàn)每個(gè)通道的輪詢檢查通道狀態(tài)的方法是可行的,但仍然是有問題的,檢查每個(gè)通道...
摘要:二原型和原型鏈其實(shí),原型這個(gè)名字本身就很容易產(chǎn)生誤解,原型在百度詞條中的釋義是指原來的類型或模型。對象的原型是通過創(chuàng)建的對象,其原型是而的原型是,從而兩個(gè)原型對象和有了繼承關(guān)系三和這是容易混淆的兩個(gè)屬性。 關(guān)于原型和原型鏈的介紹,網(wǎng)上數(shù)不勝數(shù),但能講清楚這兩個(gè)概念的很少,大多數(shù)都是介紹各種對象、屬性之間如何指來指去,最后的結(jié)果就是箭頭滿天飛,大腦一團(tuán)糟。本文將從這兩個(gè)概念的命名入手,用...
摘要:最近在學(xué)習(xí)網(wǎng)絡(luò)編程和相關(guān)的知識,了解到是模式的網(wǎng)絡(luò)框架,但是提供了不同的來支持不同模式的網(wǎng)絡(luò)通信處理,包括同步異步阻塞和非阻塞。因?yàn)榈陌姹臼褂玫牡哪J?,而則希望使用模式,而且版本沒有將的部分配置項(xiàng)暴露出來,比如說和。 ??最近在學(xué)習(xí)Java網(wǎng)絡(luò)編程和Netty相關(guān)的知識,了解到Netty是NIO模式的網(wǎng)絡(luò)框架,但是提供了不同的Channel來支持不同模式的網(wǎng)絡(luò)通信處理,包括同步、異步、...
閱讀 863·2023-04-25 15:13
閱讀 1479·2021-11-22 12:03
閱讀 880·2021-11-19 09:40
閱讀 1983·2021-11-17 09:38
閱讀 1800·2021-11-08 13:18
閱讀 703·2021-09-02 15:15
閱讀 1813·2019-08-30 15:54
閱讀 2788·2019-08-30 11:12