Java IO流深入理解
阻塞(Block)和非阻塞(Non-Block)
阻塞和非阻塞是進程在訪問數(shù)據(jù)的時候,數(shù)據(jù)是否準備就緒的一種處理方式,當數(shù)據(jù)沒有準備的時候。
**阻塞:**往往需要等待緩沖區(qū)中的數(shù)據(jù)準備好過后才處理其他的事情,否者一直等待在那里
**非阻塞:**當我們進程訪問我們的數(shù)據(jù)緩沖區(qū)的時候,如果數(shù)據(jù)沒有準備好則直接返回,不會等待。如果數(shù)據(jù)已經(jīng)準備好,也直接返回。
同步(Synchronization)和異步(Asynchronous)
同步和異步都是基于應用程序和操作系統(tǒng)處理IO事件所采用的方式。比如同步:是應用程序要直接參與IO讀寫的操作。異步:所有的IO讀寫交給操作系統(tǒng)去處理,應用程序只需要等待通知。
同步方式在處理IO事件的時候,必須阻塞在某個方法上面等待我們的IO事件完成(阻塞IO事件或者通過輪詢IO事件的方式),對于異步來說,所有的IO讀寫都交給了操作系統(tǒng)。這個時候,我們可以去做其他的事情,并不需要去完成真正的IO操作,當操作完成iO后,會給我們的應用程序一個通知。
**同步:**阻塞到IO事件,阻塞到read或者write。這個時候我們就完全不能做自己的事情。讓讀寫方法加入到線程里面,然后阻塞線程來實現(xiàn),對線程的性能開銷比較大。
BIO與NIO對比
IO模型 | BIO | NIO |
---|---|---|
通信 | 面向流(鄉(xiāng)村公路) | 面向緩沖(高速公路,多路復用技術) |
處理 | 阻塞IO(多線程) | 非阻塞IO(反應堆Reactor) |
觸發(fā) | 無 | 選擇器 輪詢機制 |
面向流與面向緩沖
java NIO和BIO之間第一個最大的區(qū)別是,BIO是面向流的,NIO是面向緩沖的。Java BIO面向流意味著每次從流中讀一個或多個字節(jié),直至讀取所有的字節(jié),它們沒有被緩存在任何地方。此外,它不能前后移動流中的數(shù)據(jù)。如果需要前后移動從流中讀取的數(shù)據(jù),需要先將它緩存到一個緩沖區(qū)。Java NIO的緩沖導向方法略有不同。數(shù)據(jù)讀取到一個它稍后處理的緩沖區(qū),需要時可在緩沖區(qū)前后移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否緩沖區(qū)包含了所有您需要處理的數(shù)據(jù)。而且,需確保當更多的數(shù)據(jù)讀入緩沖區(qū)時,不要覆蓋緩沖區(qū)里尚未處理的數(shù)據(jù)。
阻塞與非阻塞
Java BIO的各種流是阻塞的。這意味著,當一個線程調用read()和write()時,該線程被阻塞,直到有一些數(shù)據(jù)被讀取,或數(shù)據(jù)完全寫入。該線程在此期間不能再干任何事情了。java NIO的非阻塞模式,使一個線程從某通道發(fā)送請求讀取數(shù)據(jù),但是它僅能得到目前可用的數(shù)據(jù),如果目前沒有數(shù)據(jù)可用時,就什么多不會獲取。而不是保持線程阻塞,所以直至數(shù)據(jù)變得可以讀取之前,該線程可以繼續(xù)做其他的事情。
非阻塞寫也是如此。一個線程請求寫入一些數(shù)據(jù)到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。線程通常將非阻塞IO的空閑時間用于在其他通道上執(zhí)行IO操作,所以一個單獨的線程現(xiàn)在可以管理多個輸入和輸出通道。
選擇器的問世
java NIO的選擇器(Selector)允許一個單獨的線程來監(jiān)視多個輸入通道,你可以注冊多個通道使用一個選擇器,然后使用一個單獨的線程來“選擇”通道,這些通道里已經(jīng)有可以處理的輸入,或者選擇已準備寫入的通道。這種選擇機制,使得一個單獨的線程很容易來管理多個通道。
Java NIO三件套
在NIO中有幾個核心對象需要掌握:緩沖器(Buffer)
、選擇器(Selector)
、通道(Channel)
緩沖區(qū)Buffer
緩沖區(qū)實際上是一個容器對象,更直接的說,其實就是一個數(shù)組,在NIO庫中,所有數(shù)據(jù)都是用緩沖區(qū)出來的。在讀取數(shù)據(jù)時,它是直接讀到緩沖區(qū)的;在寫入數(shù)據(jù)時,它也是寫入到緩沖區(qū)的;任何時候訪問NIO中的數(shù)據(jù),都是將它放到緩沖區(qū)中。而在面向流I/O系統(tǒng)中,所有數(shù)據(jù)都是直接寫入或者直接將數(shù)據(jù)讀取到Stream對象中。
在NIO中,所有的緩沖區(qū)類型都繼承于抽象類Buffer,最常用的就是ByteBuffer,對于java中的基本類型,基本都有一個具體Buffer類型與之相對于,他們之間的extend關系如下圖所示。
eg:
public static void main(String[] args) { //new NIOServerDemo(8080).listen(); // 分配新的 int 緩沖區(qū),參數(shù)為緩沖區(qū)容量 // 新緩沖區(qū)的當前位置將為零,其界限(限制位置)將為其容量。它將具有一個底層實現(xiàn)數(shù)組,其數(shù)組偏移量將為零。 IntBuffer buffer = IntBuffer.allocate(8); for (int i = 0; i < buffer.capacity(); ++i) { int j = 2 * (i + 1); // 將給定整數(shù)寫入此緩沖區(qū)的當前位置,當前位置遞增 buffer.put(j); } // 重設此緩沖區(qū),將限制設置為當前位置,然后將當前位置設置為 0 buffer.flip(); // 查看在當前位置和限制位置之間是否有元素 while (buffer.hasRemaining()) { // 讀取此緩沖區(qū)當前位置的整數(shù),然后當前位置遞增 int j = buffer.get(); System.out.print(j + " "); } }
2 4 6 8 10 12 14 16 Process finished with exit code 0
Buffer的基本的原理
在談到緩沖區(qū)時,我們說緩沖區(qū)對象本質上是一個數(shù)組,但它其實是一個特殊的數(shù)組,緩沖區(qū)對象內(nèi)置了一些機制,能夠跟蹤和記錄緩沖區(qū)的狀態(tài)變化情況。如果我們使用get()方法從緩沖區(qū)獲取數(shù)據(jù)或者使用put()方法把數(shù)據(jù)寫入緩沖區(qū),都會引起緩沖區(qū)狀態(tài)的變化。
在緩沖區(qū)中,最重要的屬性有下面三個,它們一起合作完成對緩沖區(qū)內(nèi)部的狀態(tài)的變化跟蹤。
position: 指定下一個將要被寫入或者讀取的元素索引,它的值由get()/put()方法自動更新,在新創(chuàng)建一個Buffer對象時,position被初始化為0.
limit:指定還有多少數(shù)據(jù)需要取出(在從緩沖區(qū)寫入通道時),或者還有多少空間可以放入數(shù)據(jù)(在從通道讀入緩沖區(qū)時)
**capacity:**制定了可以存儲在緩沖區(qū)中的最大數(shù)據(jù)容量,實際上,它制定了底層數(shù)組的大小,或者至少是指定了準許我們使用的底層數(shù)組的容量。
以上三個屬性值之間有一些相對大小的關系: 0<=positon<=limit<=capacity。如果我們創(chuàng)建一個新的容量大小為10的ByteBuffer對象,在初始化的時候,positon設置為0,limit和capacity被設置為10,在以后使用ByteBuffer對象過程中,capacity的值不會再發(fā)生變化,而其他兩個將會隨著使用而變化。
eg:
package com.evan.netty.nio.demo; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; /** * @author evanYang * @version 1.0 * @date 2021/7/26 11:29 */ public class BufferDemo { public static void main(String[] args) throws IOException { FileInputStream fin = new FileInputStream("D://evan.txt"); FileChannel fc = fin.getChannel(); //先分配一個10大小的緩沖區(qū) ByteBuffer buffer = ByteBuffer.allocate(10); outPut("初始化",buffer); fc.read(buffer); outPut("調用read()方法",buffer); buffer.flip(); outPut("調用flip()",buffer); //判斷有沒有可讀數(shù)據(jù) while (buffer.remaining()>0){ byte b = buffer.get(); } outPut("調用get()",buffer); //可以理解為解鎖 buffer.clear(); outPut("調用clear()",buffer); fin.close(); } /** * 打印緩存實時狀況 * @param step * @param buffer */ public static void outPut(String step, Buffer buffer){ System.out.println(step+":"); System.out.println("capacity: "+buffer.capacity()+","); System.out.println("position: "+buffer.position()+","); System.out.println("limit: "+buffer.limit()); System.out.println(); } }
文件中的數(shù)據(jù)
輸出結果:
運行結果我們已經(jīng)可以知道,四個屬性值分別如圖所示:
我們可以從管道中讀取一些數(shù)據(jù)到緩沖區(qū),注意從通道讀取數(shù)據(jù),相當于往緩沖區(qū)寫入數(shù)據(jù)。如果讀取4個自己的數(shù)據(jù),則此時position的值為4,即下一個將要被下入的字節(jié)索引是4,而limit仍然是10,如下圖所示:
下一步把讀取的數(shù)據(jù)寫入到輸出管道中,相當于從緩沖區(qū)中讀取數(shù)據(jù),在此之前,必須調用flip()方法,該方法將會完成兩件事:
1,把limit設置為當前的positon值
2,把position設置為0
由于position被設置為0,所以可以保證在下一步輸出時讀取到的是緩沖區(qū)中的第一個字節(jié),而limit被設置為當前的position,可以保證讀取的數(shù)據(jù)正好是之前寫入到緩沖區(qū)的數(shù)據(jù),如下圖所示。
現(xiàn)在調用get()方法從緩沖區(qū)讀取數(shù)據(jù)寫入到輸出通道,這會導致position的增加而limit保持不變,單position不會超過limit的值,所以在讀取我們之前寫入到緩沖區(qū)中的4個自己之后,position和limit的值都為4.如下圖所示。
在從緩沖區(qū)讀取數(shù)據(jù)完畢后,limit的值仍然保持在我們調用flip()方法時的值,調用clean()方法能夠把所有的狀態(tài)設置為初始值。
緩沖區(qū)分配
在前面的幾個例子中,我們已經(jīng)看過了,在創(chuàng)建一個緩沖對象時,會調用靜態(tài)方法allocate()來指定緩沖區(qū)的容量,其實調用allocate()相當于創(chuàng)建一個指定大小的數(shù)組,并把它包裝為緩沖區(qū)對象?;蛘呶覀円部梢灾苯訉⒁粋€現(xiàn)有的數(shù)組,包裝為緩沖區(qū)對對象
選擇器Selector
傳統(tǒng)的Server/Client 模式會基于TPR(Thread per Request),服務器會為每個客戶端請求建立一個線程,由該線程單獨負責處理一個客戶請求。這種模式帶來的一個問題就是線程數(shù)量的劇增,大量的線程會增大服務器的開銷。大多數(shù)的實現(xiàn)為了避免這個問題,都采用了線程池模型,并設置線程池模型的最大數(shù)量,這又帶來了新的問題,如果線程池中有200個線程,而有200個用戶都在進行大文件下載,會導致第201個用戶的請求無法及時處理,即便第201個用戶只想請求一個幾kb大小的頁面。傳統(tǒng)的Server/Client模式如下圖所示。
NIO 中非阻塞I/O采用了基于Reactor模式的工作模式,I/O調用不會被阻塞,相反是注冊感興趣的特定I/O事件,如可讀數(shù)據(jù)到達,新的套接字連接等等,在發(fā)生特定事件時,系統(tǒng)在通知我們。NIO中實現(xiàn)非阻塞I/O的核心對象就是Selector,Selector就是注冊各種I/O事件地方,而且當那些事件發(fā)生時,就是這個對象告訴我們所發(fā)生的事件。如下圖所示。
從圖中可以看出,當有讀或寫等任何注冊的時間發(fā)生時,可以從Selector中獲得相應的SelectionKey,同時從SelectionKey中可以找到發(fā)生的事件和該事件所發(fā)生的具體SelectableChannel,以獲得客戶端發(fā)送過來的數(shù)據(jù)。
使用NIO中非阻塞I/O編寫服務器處理程序,大體上可以分為下面三個步驟:
1,向Selector對象注冊感興趣的事件。
2,從Selector中獲取感興趣的事件。
3,根據(jù)不同的事件進行相應的處理。
通道Channel
通道是一個對象,通過它可以讀取和寫入數(shù)據(jù),當然了所有數(shù)據(jù)都通過Buffer對象來處理。我們永遠不會將字節(jié)直接寫入通道中,相反是將數(shù)據(jù)寫入包含一個或者多個字節(jié)的緩存區(qū)。同樣不會直接從通道中讀取字節(jié),而是將數(shù)據(jù)從通道讀入緩存區(qū),再從緩沖區(qū)獲取這個字節(jié),而是將數(shù)據(jù)從通道讀入緩沖區(qū),再從緩沖區(qū)獲取這個字節(jié)。
在NIO中,提供了多種通道對象,而所有的通道對象都實現(xiàn)了Channel接口。它們之間的繼承關系如下圖所示:
使用NIO讀取數(shù)據(jù)
在前面我們說過,任何時候讀取數(shù)據(jù),都不是直接從通道讀取,而是從通道讀取到緩沖區(qū)。所以使用NIO讀取數(shù)據(jù)可以分為下面三個步驟:
1,從FileInputStream獲取Channel
2,創(chuàng)建Buffer
3,將數(shù)據(jù)從Channel讀取到Buffer中
package com.evan.netty.nio.demo; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; /** * @author evanYang * @version 1.0 * @date 2021/7/26 16:15 */ public class FileInputDemo { public static void main(String[] args) throws IOException { FileInputStream fin=new FileInputStream("D://evan.txt"); FileChannel channel = fin.getChannel(); ByteBuffer allocate = ByteBuffer.allocate(1024); //讀取數(shù)據(jù)到緩沖區(qū) channel.read(allocate); allocate.flip(); while (allocate.remaining()>0){ byte b = allocate.get(); System.out.println(b); } fin.close(); } }
使用NIO寫入數(shù)據(jù)
使用NIO寫入數(shù)據(jù)與讀取數(shù)據(jù)的過程類似,同樣數(shù)據(jù)不是直接寫入通道,而是寫入緩沖區(qū),可以分為下面三個步驟:
1,從FileputStream獲取channel。
2,創(chuàng)建Buffer
3,將數(shù)據(jù)從Channel寫入到Buffer中,
package com.evan.netty.nio.demo; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; /** * @author evanYang * @version 1.0 * @date 2021/7/26 16:33 */ public class FileOutPutDemo { static private final byte message[] ={83, 111, 109, 101, 32, 98, 121, 116, 101, 115, 46 }; public static void main(String[] args) throws IOException { FileOutputStream fout=new FileOutputStream("D://evan.txt"); FileChannel channel = fout.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(1024); for (int i = 0; i < message.length; i++) { buffer.put(message[i]); } buffer.flip(); channel.write(buffer); fout.close(); } }
IO多路復用
我們試想一下這樣的現(xiàn)實場景。
100桌客人到店點菜
方法A:
服務員都把僅有的一份菜單遞給其中一桌客人,然后站在這個客人身旁等待客人完成點菜過程。。。。。
方法B:
老板馬上新雇傭99名服務員,同時印制99本新的菜單。沒人服務一桌客人。
方法C:
改進點菜的方式,當客人到店后,自己申請一本菜單。想好自己要點的菜,然后呼叫服務員。服務員站在自己身邊記錄客人點的菜的內(nèi)容。
- 到店情況 :并發(fā)量
- 到店情況不理想時,一個服務員一本菜單,就足夠了
- 客人:服務端請求
- 點餐內(nèi)容:客服端發(fā)送的實際數(shù)據(jù)
- 老板:操作系統(tǒng)
- 人力成本:系統(tǒng)資源
- 菜單:文件狀態(tài)描述符(FD)。操作系統(tǒng)對于一個進程能夠同時持有的文件狀態(tài)描述符的個數(shù)是有限制的,在linux系統(tǒng)中,$Ulimit -n 查看這個限制值,當然也是可以(并且應該)進行內(nèi)核參數(shù)調整的
- 服務員:操作系統(tǒng)內(nèi)核用于IO操作的線程(內(nèi)核線程)
- 廚師:應用程序線程(當然廚房就是應用程序進程)
- 方法A:同步IO
- 方法B:同步IO
- 方法C:多路復用IO
總結
本篇文章就到這里了,希望能給你帶來幫助,也希望您能夠多多關注腳本之家的更多內(nèi)容!
相關文章
springboot?vue項目管理前后端實現(xiàn)編輯功能
這篇文章主要為大家介紹了springboot?vue項目管理前后端實現(xiàn)編輯功能,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-05-05詳解Spring Cloud Stream使用延遲消息實現(xiàn)定時任務(RabbitMQ)
這篇文章主要介紹了詳解Spring Cloud Stream使用延遲消息實現(xiàn)定時任務(RabbitMQ),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-01-01spring?cloud?gateway中配置uri三種方式
gateway?組件是SpringCloud?組件中的網(wǎng)關組件,主要是解決路由轉發(fā)的問題,跟nginx有點類似,區(qū)別是nginx多用在前端上,gateway用在后端上,本文給大家介紹的非常詳細,需要的朋友參考下吧2023-08-08