BIO,NIO,AIO
BIO NIO AIO
1.什麼是BIO?
bio:b為block,jdk 1.0 中的io體系是阻塞的
nio:n為non-block,針對block而言。就是非阻塞IO的意思
aio:a為asynchronous,非同步的,非同步io
發展歷程:bio(jdk1.0) -> nio(jdk1.4) -> aio(jdk1.7)
所謂IO即input和output的縮寫,是對資料的流入和流出的一種抽象,程式設計中很常見的一個概念。
BIO就是食堂排隊打飯,排隊期間不能做別的事情。
由這四個類派生出來的子類名稱都是以其父類名作為子類名的字尾,如InputStream的子類FileInputStream,Reader的子類FileReader。
程式中的輸入輸出都是以流的形式儲存的,流中儲存的實際上全都是位元組檔案。
java中的阻塞式方法是指在程式呼叫改方法時,必須等待輸入資料可用或者檢測到輸入結束或者丟擲異常,否則程式會一直停留在該語句上,不會執行下面的語句。比如read()和readLine()方法。
什麼是位元組流?什麼是字元流?
位元組是佔1個Byte,即8位;
字元是佔2個Byte,即16位。
java的位元組是有符號型別,而字元是無符號型別!
什麼時候該用位元組流,什麼時候用字元流?
非純文字資料:使用位元組流(xxxStream)。比如讀取圖片 ,表情包
純文字資料:使用字元流(xxxReader/xxxWriter),
最後其實不管什麼型別檔案都可以用位元組流處理,包括純文字,但會增加一些額外的工作量。所以還是按原則選擇最合適的流來處理
小結:
1)判斷操作的資料型別
純文字資料:讀用Reader系,寫用Writer系
非純文字資料:讀用InputStream系,寫用OutputStream系
如果純文字資料只是簡單的複製,下載,上傳,不對資料內容本身做處理,那麼使用Stream系
2)判斷操作的物理節點
記憶體:ByteArrayXXX
硬碟:FileXXX
網路:http中的request和response均可獲取流物件,tcp中socket物件可獲取流物件
鍵盤(輸入裝置):System.in
顯示器(輸出裝置):System.out
3)搞清讀寫順序,一般是先獲取輸入流,從輸入流中讀取資料,然後再寫到輸出流中。
4)是否需增加特殊功能,如需要用緩衝提高讀寫效率則使用BufferedXXX,如果需要獲取文字行號,則使用LineNumberXXX,如果需要轉換流則使用InputStreamReader和OutputStreamWriter,如果需要寫入和讀取物件則使用ObjectOutputStream和ObjectInputStream
2.什麼是NIO?
Nio雖然還是銀行取錢,但是是有一張小票,可以詢問銀行經理檢視當前進度(是否輪到自己取錢了,當資料準備完畢時,就取錢的意思)
答:看了一些文章,傳統的IO流是阻塞式的,會一直監聽一個ServerSocket,在呼叫read等方法時,他會一直等到資料到來或者緩衝區已滿時才返回。呼叫accept也是一直阻塞到有客戶端連線才會返回。每個客戶端連線過來後,服務端都會啟動一個執行緒去處理該客戶端的請求。並且多執行緒處理多個連線。每個執行緒擁有自己的棧空間並且佔用一些 CPU 時間。每個執行緒遇到外部未準備好的時候,都會阻塞掉。阻塞的結果就是會帶來大量的程序上下文切換。
對於NIO,它是非阻塞式,核心類:
1.Buffer為所有的原始型別提供 (Buffer)快取支援。
2.Charset字符集編碼解碼解決方案
3.Channel一個新的原始 I/O抽象,用於讀寫Buffer型別,通道可以認為是一種連線,可以是到特定裝置,程式或者是網路的連線。
3.什麼是AIO?
AIO有點像叫外賣的過程,先給店家打電話說想吃什麼菜,店家回覆知道了,這個時候電話以及掛了,等待店家準備好菜就送上門打電話給使用者說要的以及到達了,可以開飯啦。
原理是基於事件回撥機制。
何為上下文切換?
當一個執行緒的時間片用完後或者其他自身原因被迫暫停運行了,這時候,另外一個執行緒或者、程序或者其他程序的執行緒就會白作業系統選中,用來佔用處理器。這種一個執行緒被暫停,一個執行緒包選中開始執行的過程就叫做上下文切換。
4.讀寫效能問題
流的讀寫是比較耗時的操作,因此為了提高效能,便有緩衝的這個概念(什麼是緩衝?假如你是個搬磚工,你工頭讓你把1000塊磚從A點運到B點,你可以一次拿一塊磚從A點運到B點放下磚,這樣你要來回跑1000次,大多數的時間開銷在路上了;你還可以使用一輛小車,在A點裝滿一車的磚,然後運到B點放下磚,如果一車最多可以裝500塊,那麼你來回兩次便可以把這些磚運完。這裡的小車便是那個緩衝)。這裡的裝貨可以理解成讀操作,卸貨可以理解成寫操作。
在java bio中使用緩衝一般有兩種方式。一種是自己申明一個緩衝陣列,利用這個陣列來提高讀寫效率;另一種方式是使用jdk提供的處理流BufferedXXX類。下面我們分別演示不使用緩衝讀寫,使用自定義的緩衝讀寫,使用BufferedXXX緩衝讀寫一個檔案。
無緩衝讀寫
/**
* 拷貝檔案(方法一)
* @param src 被拷貝的檔案
* @param dest 拷貝到的目的地
*/
public static void copyByFileStream(File src,File dest){
FileInputStream fis = null;
FileOutputStream fos = null;
long start = System.currentTimeMillis();
try {
fis = new FileInputStream(src);
fos = new FileOutputStream(dest);
int b = 0;
while((b = fis.read()) != -1){//一個位元組一個位元組的讀
fos.write(b);//一個位元組一個位元組的寫
}
} catch (Exception e) {
e.printStackTrace();
} finally{
close(fis,fos);
}
System.out.println("使用FileOutputStream拷貝大小"+getSize(src)+"的檔案未使用緩衝陣列耗時:"+(System.currentTimeMillis()-start)+"毫秒");
}
自定義陣列緩衝讀寫
/**
* 拷貝檔案(方法二)
* @param src 被拷貝的檔案
* @param dest 拷貝到的目的地
* @param size 緩衝陣列大小
*/
public static void copyByFileStream(File src,File dest,int size){
FileInputStream fis = null;
FileOutputStream fos = null;
long start = System.currentTimeMillis();
try {
fis = new FileInputStream(src);
fos = new FileOutputStream(dest);
int b = 0;
byte[] buff = new byte[size];//定義一個緩衝陣列
//讀取一定量的資料(read返回值表示這次讀了多少個數據)放入陣列中
while((b = fis.read(buff)) != -1){
fos.write(buff,0,b);//一次將讀入到陣列中的有效資料(索引[0,b]範圍的資料)寫入輸出流中
}
} catch (Exception e) {
e.printStackTrace();
} finally{
close(fos,fis);
}
System.out.println("使用FileOutputStream拷貝大小"+getSize(src)+"的檔案使用了緩衝陣列耗時:"+(System.currentTimeMillis()-start)+"毫秒,生成的目標檔案大小為"+getSize(dest));
}
BufferedXXX類緩衝讀寫
/**
* 拷貝檔案(方法三)
* @param src
* @param dest
*/
public static void copyByBufferedStream(File src,File dest) {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
long start = System.currentTimeMillis();
try{
bis = new BufferedInputStream(new FileInputStream(src));
bos = new BufferedOutputStream(new FileOutputStream(dest));
int b = 0;
while( (b = bis.read())!=-1){
bos.write(b);//使用BufferedXXX重寫的write方法進行寫入資料。該方法看似未緩衝實際做了緩衝處理
}
bos.flush();
}catch(IOException e){
e.printStackTrace();
}finally{
close(bis,bos);
}
System.out.println("使用BufferedXXXStream拷貝大小"+getSize(src)+"的檔案使用了緩衝陣列耗時:"+(System.currentTimeMillis()-start)+"毫秒");
}
方法測試:
public static void main(String[] args) {
File src = new File("E:\\iotest\\1.bmp");
File dest = new File("E:\\iotest\\1_copy.bmp");
//無緩衝區
copyByFileStream(src,dest);
sleep(1000);
//32位元組緩衝區
copyByFileStream(src,dest,32);
sleep(1000);
//64位元組緩衝區
copyByFileStream(src,dest,64);
sleep(1000);
//BufferedOutputStream緩衝區預設大小為8192位元組 =1024*8 也就是8K緩衝區
copyByBufferedStream(src, dest);
sleep(1000);
//BufferedOutputStream緩衝區預設大小為8192*2位元組 16K緩衝區
copyByBufferedStream(src, dest, 8192*2);
}
//我本地測試如下:
使用FileOutputStream拷貝大小864054位元組的檔案未使用緩衝陣列耗時:5092毫秒,生成的目標檔案大小為864054位元組
使用FileOutputStream拷貝大小864054位元組的檔案使用了緩衝陣列耗時:215毫秒,生成的目標檔案大小為864054位元組
使用FileOutputStream拷貝大小864054位元組的檔案使用了緩衝陣列耗時:124毫秒,生成的目標檔案大小為864054位元組
使用BufferedXXXStream拷貝大小864054位元組的檔案使用了緩衝陣列耗時:41毫秒,生成的目標檔案大小為864054位元組
使用BufferedXXXStream拷貝大小864054位元組的檔案使用了緩衝陣列耗時:8毫秒,生成的目標檔案大小為864054位元組
5.熟悉select,pull,epull
Linux 下有三種提供 I/O 多路複用的 API,分別是:select、poll、epoll。
一個程序雖然任一時刻只能處理一個請求,但是處理每個請求的事件時,耗時控制在 1 毫秒以內,這樣 1 秒內就可以處理上千個請求,把時間拉長來看,多個請求複用了一個程序,這就是多路複用,這種思想很類似一個 CPU 併發多個程序,所以也叫做時分多路複用。
我們熟悉的 select/poll/epoll 核心提供給使用者態的多路複用系統呼叫,程序可以通過一個系統呼叫函式從核心中獲取多個事件。
select/poll/epoll 是如何獲取網路事件的呢?在獲取事件時,先把所有連線(檔案描述符)傳給核心,再由核心返回產生了事件的連線,然後在使用者態中再處理這些連線對應的請求即可。
select/poll
select 實現多路複用的方式是,將已連線的 Socket 都放到一個檔案描述符集合,然後呼叫 select 函式將檔案描述符集合拷貝到核心裡,讓核心來檢查是否有網路事件產生,檢查的方式很粗暴,就是通過遍歷檔案描述符集合的方式,當檢查到有事件產生後,將此 Socket 標記為可讀或可寫, 接著再把整個檔案描述符集合拷貝回用戶態裡,然後使用者態還需要再通過遍歷的方法找到可讀或可寫的 Socket,然後再對其處理。
所以,對於 select 這種方式,需要進行 2 次「遍歷」檔案描述符集合,一次是在核心態裡,一個次是在使用者態裡 ,而且還會發生 2 次「拷貝」檔案描述符集合,先從使用者空間傳入核心空間,由核心修改後,再傳出到使用者空間中。
select 使用固定長度的 BitsMap,表示檔案描述符集合,而且所支援的檔案描述符的個數是有限制的,在 Linux 系統中,由核心中的 FD_SETSIZE 限制, 預設最大值為 1024
,只能監聽 0~1023 的檔案描述符。
poll 不再用 BitsMap 來儲存所關注的檔案描述符,取而代之用動態陣列,以連結串列形式來組織,突破了 select 的檔案描述符個數限制,當然還會受到系統檔案描述符限制。
但是 poll 和 select 並沒有太大的本質區別,都是使用「線性結構」儲存程序關注的 Socket 集合,因此都需要遍歷檔案描述符集合來找到可讀或可寫的 Socket,時間複雜度為 O(n),而且也需要在使用者態與核心態之間拷貝檔案描述符集合,這種方式隨著併發數上來,效能的損耗會呈指數級增長。
1.拷貝到核心-
2.遍歷+標記可讀或科協
3.拷貝到使用者態
4.遍歷可讀或可寫
epoll-採用紅黑樹
epoll 通過兩個方面,很好解決了 select/poll 的問題。
第一點,epoll 在核心裡使用紅黑樹來跟蹤程序所有待檢測的檔案描述字,把需要監控的 socket 通過 epoll_ctl()
函式加入核心中的紅黑樹裡,紅黑樹是個高效的資料結構,增刪查一般時間複雜度是 O(logn)
,通過對這棵黑紅樹進行操作,這樣就不需要像 select/poll 每次操作時都傳入整個 socket 集合,只需要傳入一個待檢測的 socket,減少了核心和使用者空間大量的資料拷貝和記憶體分配。
紅黑樹記錄待檢測檔案描述字。也就是入口控制,不需要遍歷整個socket
第二點, epoll 使用事件驅動的機制,核心裡維護了一個連結串列來記錄就緒事件,當某個 socket 有事件發生時,通過回撥函式核心會將其加入到這個就緒事件列表中,當用戶呼叫 epoll_wait()
函式時,只會返回有事件發生的檔案描述符的個數,不需要像 select/poll 那樣輪詢掃描整個 socket 集合,大大提高了檢測的效率。
連結串列記錄就緒事件,出口控制,不需要遍歷整個socket
什麼是紅黑樹?
自平衡二叉查詢樹,實現關聯陣列。在插入和刪除時,通過特定操作保持二叉查詢樹的平衡,從而獲得較高查詢效能
不破不立,怕啥懟啥!