1. 程式人生 > 其它 >java春招面試衝刺系列:IO詳細解析

java春招面試衝刺系列:IO詳細解析

技術標籤:Java面試linuxjava多執行緒

IO簡介

IO是Java中的一種輸入和輸出的功能,Java中對這種操作叫做對流的操作。
流代表的是任何有能力產出資料的資料來源物件或者是有能力接受資料的接收端物件。
流的本質是資料傳輸,流不只是對檔案可進行讀寫,還可以對記憶體、網路、程式操作。

學習筆記

NIO是同步的IO,是因為程式需要IO操作時,必須獲得了IO許可權後親自進行IO操作才能進行下一步操作。AIO是對NIO的改進(所以AIO又叫NIO.2),它是基於Proactor模型的。每個socket連線在事件分離器註冊 IO完成事件 和 IO完成事件處理器。程式需要進行IO時,向分離器發出IO請求並把所用的Buffer區域告知分離器,分離器通知作業系統進行IO操作,作業系統自己不斷嘗試獲取IO許可權並進行IO操作(資料儲存在Buffer區),操作完成後通知分離器;分離器檢測到 IO完成事件,則啟用 IO完成事件處理器,處理器會通知程式說“IO已完成”,程式知道後就直接從Buffer區進行資料的讀寫。

也就是說:AIO是發出IO請求後,由作業系統自己去獲取IO許可權並進行IO操作;NIO則是發出IO請求後,由執行緒不斷嘗試獲取IO許可權,獲取到後通知應用程式自己進行IO操作。
同步/非同步:資料如果尚未就緒,是否需要等待資料結果。
阻塞/非阻塞:程序/執行緒需要操作的資料如果尚未就緒,是否妨礙了當前程序/執行緒的後續操作。應用程式的呼叫是否立即返回!
NIO與BIO最大的區別是 BIO是面向流的,而NIO是面向Buffer的。

Buffer是一塊連續的記憶體塊,是 NIO 資料讀或寫的中轉地。 為什麼說NIO是基於緩衝區的IO方式呢?因為,當一個連結建立完成後,IO的資料未必會馬上到達,為了當資料到達時能夠正確完成IO操作,在BIO(阻塞IO)中,等待IO的執行緒必須被阻塞,以全天候地執行IO操作。為了解決這種IO方式低效的問題,引入了緩衝區的概念,當資料到達時,可以預先被寫入緩衝區,再由緩衝區交給執行緒,因此執行緒無需阻塞地等待IO。

緩衝區實際上是一個容器物件,更直接的說,其實就是一個數組,在NIO 庫中,所有資料都是用緩衝區處理的。在讀取資料時,它是直接讀到緩衝區中的; 在寫入資料時,它也是寫入到緩衝區中的;任何時候訪問NIO 中的資料,都是將它放到緩衝區中。而在面向流I/O 系統中,所有資料都是直接寫入或者直接將資料讀取到Stream 物件中。在NIO 中,所有的緩衝區型別都繼承於抽象類Buffer,最常用的就是ByteBuffer

Java IO中常用的類

整個Java IO包中最重要的就是5個類和一個介面。
5個類指:
    * File:用於檔案或者目錄的描述資訊,例如生成新的目錄,修改檔名,刪除檔案,判斷檔案,過濾檔案等
    * OutputStream:抽象類,基於位元組的輸出操作,是所有輸出流的父類。
    * InputStream:抽象類,基於位元組的輸入操作,是所有輸入流的父類。
    * Writer:抽象類,基於字元的輸出操作。
    * Reader:抽象類,基於字元的輸入操作。
一個介面指:Serializable
另外一個特殊的類:RandomAccessFile:隨機檔案操作,可以從檔案任意位置進行存取(輸入輸出)操作。

IO介面和類的結構圖可參考技術棧圖

RandomAccessFile

我們在對檔案的操作過程中,除了使用位元組流和字元流的方式之外,我們還可以使用RandomAcessFile這個工具類來實現。
RandomAccessFile可以實現對檔案的讀 和 寫,但是他並不是繼承於以上4中基本虛擬類。
而且在對檔案的操作中,RandomAccessFile有一個巨大的優勢,他可以支援檔案的隨機訪問,程式快可以直接跳轉到檔案的任意地方來讀寫資料。所以如果需要訪問檔案的部分內容,而不是把檔案從頭讀到尾,使用RandomAccessFile將是更好的選擇。
RandomAccessFile的方法雖然多,但它有一個最大的侷限,就是隻能讀寫檔案,不能讀寫其他IO節點。
RandomAccessFile的一個重要使用場景就是網路請求中的多執行緒下載及斷點續傳。

字元與位元組

Java中有輸入和輸出兩種IO流,每種輸入輸出流又分為位元組流和字元流兩大類。

  • 關於位元組:每個位元組(byte)有8bit組成
  • 關於字元:一個字元代表一個英文字母或一個漢字

字元與位元組的關係

Java採用unicode編碼,2個位元組表示1個字元

總結

  • 先進先出,最先寫入輸出流的資料最先被輸入流讀取到
  • 順序讀取,不能隨機訪問資料(RandomAccessFile除外)
  • 只讀只寫,每個流只能是輸入流或輸出流的一種
  • 每次進行IO操作,要手動close,因為IO資源並不屬於記憶體資源,並不會被GC回收
  • 對於輸出操作,flush()會重新整理輸出流,強制緩衝區中的輸出位元組被寫出; close()關閉輸出流,釋放和這個流相關的系統資源,呼叫close()會自動flush
  • 流結束的判斷:方法read()的返回值為-1時;readLine()的返回值為null時
  • 節流沒有緩衝區,是直接輸出的,而字元流是輸出到緩衝區的。因此在輸出時,位元組流不呼叫colse()方法時,資訊已經輸出了,而字元流只有在呼叫close()方法關閉緩衝區時,資訊才輸出。要想字元流在未關閉時輸出資訊,則需要手動呼叫flush()方法
  • 位元組流與字元流區別
    • 位元組流以位元組(8bit)為單位,字元流以字元為單位,根據碼錶對映字元,一次可能讀多個位元組
    • 位元組流能處理所有型別的資料(如圖片、avi等),而字元流只能處理字元型別的資料
    • 只要是處理純文字資料,就優先考慮使用字元流。除此之外都使用位元組流

Java IO與IO的區別和比較

NIO

傳統的 Socket 阻塞模式直接導致每個 Socket 都必須繫結一個執行緒來操作資料,參與通訊的任意一方如果處理資料的速度較慢,則都會直接拖累另一方,導致另一方的執行緒不得不浪費大量的時間在 I/O 等待上,所以,每個 Socket 要繫結一個單獨的執行緒正是傳統Socket 阻塞模式的根本“缺陷”。之所以這裡加了“缺陷”兩個字,是因為這種模式在一些特定場合下效果是最好的,比如只有少量的 TCP 連線通訊,雙方都非常快速地傳輸資料,此時這種模式的效能最高。

現在我們可以開始分析“非阻塞”模式了,它就是要解決 I/O 執行緒與 Socket 解耦的問題,因此,它引入了事件機制來達到解耦的目的。我們可以認為 NIO 底層中存在一個 I/O 排程執行緒,它不斷掃描每個 Socket 的緩衝區,當發現寫入緩衝區為空(或者不滿)的時候,它會產生一個Socket 可寫事件,此時程式就可以把資料寫入 Socket 裡,如果一次寫不完,則等待下次可寫事件的通知;而當發現讀取緩衝區裡有資料的時候,它會產生一個 Socket 可讀事件,程式收到這個通知事件時,就可以從 Socket 讀取資料了。

核心空間、使用者空間、計算機體系結構、計算機組成原理、…… 確實有點兒深奧。

我的新書《程式碼之謎》會有專門的章節講解相關知識,現在寫個簡短的科普文:

就速度來說 CPU > 記憶體 > 硬碟

I- 就是從硬碟到記憶體
O- 就是從記憶體到硬碟

第一種方式:我從硬碟讀取資料,然後程式一直等,資料讀完後,繼續操作。這種方式是最簡單的,叫阻塞IO。

第二種方式:我從硬碟讀取資料,然後程式繼續向下執行,等資料讀取完後,通知當前程式(對硬體來說叫中斷,對程式來說叫回調),然後此程式可以立即處理資料,也可以執行完當前操作在讀取資料。

在一起的 Java IO 中,都是阻塞式 IO,NIO 引入了非阻塞式 IO。

還有一種就是同步 IO 和非同步 IO。經常說的一個術語就是“非同步非阻塞”,好象非同步和非阻塞是同一回事,這大概是一個誤區吧。

至於 Java NIO 的 Selector,在舊的 Java IO 系統中,是基於 Stream 的,即“流”,流式 IO。

當程式從硬碟往記憶體讀取資料的時候,作業系統使用了 2 個“小伎倆”來提高效能,那就是預讀,如果我讀取了第一扇區的第三磁軌的內容,那麼你很有可能也會使用第二磁軌和第四磁軌的內容,所以作業系統會把附近磁軌的內容提前讀取出來,放在記憶體中,即快取。

(PS:以上過程簡化了)

通過上面可以看到,作業系統是按塊 Block從硬碟拿資料,就如同一個大臉盆,一下子就放入了一盆水。但是,當 Java 使用的時候,舊的 IO 確實基於 流 Stream的,也就是雖然作業系統給我了一臉盆水,但是我得用吸管慢慢喝。

於是,NIO 橫空出世。

總結

Java中的IO/NIO:多路複用 IO 模型
1、多路複用 IO 模型是目前使用得比較多的模型。Java NIO 實際上就是多路複用 IO。在多路複用 IO 模型中,會有一個執行緒不斷去輪詢多個 socket 的狀態,只有當 socket 真正有讀寫事件時,才真正呼叫實際的 IO 讀寫操作。因為在多路複用 IO 模型中,只需要使用一個執行緒就可以管理多個socket,系統不需要建立新的程序或者執行緒,也不必維護這些執行緒和程序,並且只有在真正有 socket 讀寫事件進行時,才會使用 IO 資源,所以它大大減少了資源佔用。在 Java NIO 中,是通過 selector.select()去查詢每個通道是否有到達事件,如果沒有事件,則一直阻塞在那裡,因此這種方式會導致使用者執行緒的阻塞。多路複用 IO 模式,通過一個執行緒就可以管理多個 socket,只有當 socket 真正有讀寫事件發生才會佔用資源來進行實際的讀寫操作。因此,多路複用 IO 比較適合連線數比較多的情況。
2、另外多路複用 IO 為何比非阻塞 IO 模型的效率高是因為在非阻塞 IO 中,不斷地詢問 socket 狀態時通過使用者執行緒去進行的,而在多路複用 IO 中,輪詢每個 socket 狀態是核心在進行的,這個效率要比使用者執行緒要高的多。
3、不過要注意的是,多路複用 IO 模型是通過輪詢的方式來檢測是否有事件到達,並且對到達的事件逐一進行響應。因此對於多路複用 IO 模型來說,
一旦事件響應體很大,那麼就會導致後續的事件遲遲得不到處理,並且會影響新的事件輪詢。
I/O複用是多路複用,這裡的多路是指N個連線,每一個連線對應一個channel,或者說多路就是多個channel。複用,是指多個連線複用了一個執行緒或者少量執行緒(在Tomcat中是Math.min(2,Runtime.getRuntime().availableProcessors()))。

Reference