02.25 高級程序員應該理解的Java NIO設計理念和模型


高級程序員應該理解的Java NIO設計理念和模型


前言

前面我簡單說了一下Java I/O的內容,還是有很多小夥伴反應有些內容還是理解的不是很清晰,特別是關於Java IO的流以及NIO中的緩衝區,通道和選擇器等,它們到底是怎樣的關係。

在這篇文章中我就對Java的這兩個版本的輸入輸出支持設計說一下我的理解,希望對各位正在學習的小夥伴有所幫助。

因為關於技術的具體實現細節可以查看相關的技術文檔有具體的說明,但是我們發現小夥伴們在研究技術文檔時容易迷失在技術細節裡,造成只見樹木不見森林,用了好久的技術還說不出個所以然來。所以,我還是繼續以我的理解思路來講,技術脈絡和聯繫,不談細節。

Java對操作系統I/O的支持類型

我們知道所有應用程序的運行都是操作系統上,用Java語言開發應用程序時,通過JVM進程調用操作系統的內容處理句柄來跟操作系統通信。

我們的應用程序需要跟操作系統進行I/O操作一般有文件系統,網絡數據流,內存等三類。

第一個是文件系統,我們知道操作系統都有一個文件系統組件,來管理數據的存儲。

當然由於操作系統的不同,文件系統的格式有所不同。比如Unix/Linux等只能掛載一個文件系統,而Windows操作系統則是通過不同的盤驅來掛載不同的文件系統。


高級程序員應該理解的Java NIO設計理念和模型


文件系統一般都是有一個或者多個根目錄,以根目錄開始的路徑我們稱之為絕對路徑,以某個路徑為基礎的路徑表示我們稱之為相對路徑。

當然這裡Unix/Linux操作系統只有一個根目錄,而Windows操作系統由於可以有多個盤驅,所以可以掛載多個根目錄。

除了文件系統,我們還有一個重要的數據來源,網絡,操作系統通過網卡設備和其驅動程序來管理網絡數據流。因為是基於硬件上的網絡數據流,所以是二進制數據。

如此我們的操作系統會通過跟網卡交互來拷貝這些數據到操作系統管理的系統內存空間,然後再拷貝到具體的應用程序內存空間而跟應用程序進行交換數據。

所以,涉及到網絡的數據,我們在Java中一般使用抽象網卡的IP地址和端口組合類,並定義了協議解析套接字定義類來處理。

至於直接跟操作系統的內存交互,內存空間的形式就是字節數組,我們可以根據數組的索引來對內存空間進行交互。為此Java語言對其進行了抽象定義。

將對底層操作系統的交互接口封裝成相關的流類,來負責跟操作系統進行交互。

我們知道文件系統是基於磁盤的固定存儲塊的來進行抽象和算法設計的。也就是說我們的應用程序跟文件系統的交互是基於塊的數據流。

由於我們應用程序需要的數據類型定義跟文件存儲塊的大小設計之間存在差異,所以需要操作系統控制的內存空間來作為中介將文件系統存儲數據塊跟應用程序數據之間錯配進行調整,從而能夠使得兩者之間進行交互,包括數據讀取和寫入等。


高級程序員應該理解的Java NIO設計理念和模型


而對於需要跟網絡數據進行交互的,我們知道網絡數據是二進制數據,而且來自網絡的數據有可能是不穩定的連續二進制數據流。

為此我們必須有一個操作系統管理的內存緩衝空間,來從網卡上接收數據,從而將基於流的數據根據約定的傳輸協議來解析為應用程序需要的數據類型。

這個過程由於網絡的不穩定,以及其他原因,所以這種基於流的數據輸入輸出速度要比上面我們說的基於塊的數據輸入輸出速度要慢的多。

經典Java I/O

對於跟字節數組的交互通信涉及到的輸入輸出非常簡單就是其實就是對底層數組的操作,它一般用於應用程序內部線程間的數據輸入輸出。Java為此定義了ByteArrayInputStream和ByteArrayOutputStream兩個子類流。

它們同樣提供調用底層數據讀取和寫入指令接口,只是每次操作的是字節數組。

另外,對於數據流來說,我們知道一般通過InputStream流形式從外部數據來讀取數據到應用程序,而使用OutputStream流形式來將應用程序數據寫入到目標插槽中。

同樣,我們可以將多個流鏈接起來,形成各種數據的管線方式。Java為此定義了PipedInputStream和PipedOutputStream等類型來提供連接數據流建立數據流管線能力。


高級程序員應該理解的Java NIO設計理念和模型


Java對於流的定義是一個任意長度的有序字節序列。

最初,我們的操作系統對於數據流的操作是每次只能夠操作一個字節。也就是說我們的數據流應該是按照字節來進行讀入和寫出操作。

但是我們編寫應用程序所需要定義的數據卻是各種長度類型的,所以就需要將這些字節數據根據一定的規則轉換為各種類型數據。這個過程需要操作系統藉助其管理的內存空間來完成。

當然我們會在字節數據流的基礎上,為其設定一個存儲字節數組,然後根據數組長度來對數據進行緩衝,然後一起處理。

如此我們就不用再每次只操作一個字節,而是操作指定緩衝長度的字節,這體現為我們定義的一些基礎類型數據流。而這些都是建立在Stream增加過濾功能設計基礎上的。

我在前面的文章中說到要學好Java對於輸入/輸出數據操作的支持定義,必須掌握一種設計模式,那就是裝飾器模式。

在這種模式下,我們會在將一些基礎的核心操作抽象為統一的接口或抽象類,在Java IO中,這個基礎核心就是InputStream和OutputStream,在這兩個接口中我們定義了對於一個內存空間的基礎操作可以調用的內容。

但是它的成員映射的都是對於底層操作系統的基礎操作指令調用。

為了滿足我們應用程序開發的需要,我們需要對這些基礎核心操作接口進一步包裝,讓其能夠提高我們應用程序需要的數據流操作。

為了實現這一點,我們就定義一些抽象類來實現這一接口,同時定義一些擴展的功能接口,並在實現這些擴展接口時能夠利用核心基礎操作的調用。


高級程序員應該理解的Java NIO設計理念和模型


如此我們就可以在具體實現這些抽象類或者接口時獲得各種具體的類型定義,但是都共同實現基礎的核心功能。

同時我們還能夠在各個實現子類中去添加一些對基礎核心操作的封裝轉換方法,這就是裝飾模式的目的。

而整個Java IO內容都是基於基本的流操作,然後增加了擴展方法接口來實現我們應用程序需要的具體數據流創建。

Java NIO設計原理

在瞭解了傳統的Java 對於IO的支持定義後,我們再來看一下在新一代IO支持中,對於流概念的進一步封裝,為了借用現代計算機多處理器或者多內核處理器的優點,同時克服傳統IO需要我們CPU來處理每個字節的接收,如果沒有數據輸入則會被阻塞等待,直到輸入輸出數據完成才能開始其它任務。

我們將對於Stream流的操作和操作目標封裝到一個進程中,同時基於操作系統的特點而設計一個獨立的進程來對這些工作進程進行監控,同時將大部分的IO操作交由現代計算機系統的輸入輸出設備獨立完成。

為此Java在其NIO中抽象出了新的概念:緩衝區和管道。緩衝區相當於一個在應用程序管理的內存中定義一定緩衝空間,用於應用程序操作數據使用。而管道Channel則是封裝並優化了對這些內存空間的具體操作並將指令接口暴露出來,讓我們不用在去考慮流的概念,而是使用通道的概念。

由於我們的現代計算機結構中,輸入輸出設備具備自己的控制器,完全可以脫離CPU而獨立的進行主內存和外部設備之間的數據交互操作。

由此我們開編寫應用程序時,只需要向操作系統提交對應的讀寫指令,而具體的操作可以由操作系統轉交給輸入輸出設備控制器通過DMA技術完成對數據的操作。


高級程序員應該理解的Java NIO設計理念和模型


而我們的CPU可以不用被阻塞等待完成,去完成其他任務。直到我們的輸入輸出設備觸發完成或者異常事件中斷,再由CPU進行相應的處理。

由一個獨立的監控線程來監控各個通道是否準備好讀入或者寫出數據並觸發相應的操作,如此我們就可以充分利用操作系統的多路複用特性,這裡在設計時,為了在我們應用程序中提供對應的數據和接口,Java定義了就緒選擇機制實現的Seletor概念。

它封裝的需要的數據和相關操作定義,以此讓我們的應用程序的操作跟底層操作系統的關於輸入輸出數據操的各類事件對應。

總結

綜上,我大概的串講了一下Java對於輸入輸出功能的編程支持。其實要理解Java的IO和NIO等設計,需要對計算機硬件和操作系統對於輸入輸出功能的原理有一定的瞭解才可以。

這些原理會告訴你,我們的應用程序其實就是對各種不同數據流的組合和處理來完成業務功能的實現的。

在經典的IO時代,我們對流Stream的不同組合處理數據反映業務邏輯的設計,而到了Java NIO時代我們藉助內存緩衝區,通道和就續選擇器來抽象操作對象。

我們在經典IO時代,通過不同的流管線組合來處理複雜的業務,而到了NIO時代,我們通過組合不同的通道Channel來對應用程序的複製業務邏輯進行分拆和數據處理。

在處理數據時我們應該首先區分我們操作的數據格式是以塊為單位讀取的文件類數據流還是以單字節為單位讀取的網絡數據流。其實這兩類在我們應用程序編寫過程中用到的都不少。

特別是操作系統的內存分頁管理,讓我們對於文件映射這些技術來提高塊數據的讀取效率非常有用。

而對於網絡數據流的操作,充分利用緩衝區和多路複用技術,可以大大提高網絡數據流的處理性能。


分享到:


相關文章: