1. 程式人生 > 其它 >BIO,NIO,AIO

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

什麼是紅黑樹?

自平衡二叉查詢樹,實現關聯陣列。在插入和刪除時,通過特定操作保持二叉查詢樹的平衡,從而獲得較高查詢效能

不破不立,怕啥懟啥!