Java NIO_I/O基本概念_Java中的缓冲区(Buffer)_通道(Channel)_网络I/O
I/O基本概念
緩沖區基礎
緩沖區是I/O的基礎, 進程使用read(), write()將數據讀出/寫入從緩沖區中; 當緩沖區寫滿, 內核向磁盤發出指令, 將緩沖區中數據寫入磁盤中(這一步不需要CPU), 當磁盤控制器將緩沖區裝滿, 內核將緩沖區數據拷貝到進程中指定的緩沖區; 操作如下圖:
當中忽略了很多細節, 只涉及簡單的步驟
上面的進程通常是用戶進程, 需要指出的一點就是, 當內核接受到read指令的時候, 首先會去內核內部的緩沖區尋找所需數據, 如果所需的數據不在緩沖區中, 那么用戶進程將被掛起, 直到緩沖區中存在數據, 最后內核再將緩沖區中數據拷貝到進程內部的緩沖區中.
注:
1.使用內核的意義:
2.讀取過程存在的優化方案:
1.進程將所有緩沖區的內存地址交給內核. 進程read操作: 內核根據緩沖區地址, 將數據發散到進程中每個緩沖區的; 進程write操作: 將進程中每個緩沖區的數據集聚, 然后一起存入內核緩沖區; 這樣做避免了每一次讀寫操作都要進行磁盤操作, 減少性能損耗 2.使用虛擬內存. 將內核緩沖區地址與進程緩沖區地址映射到同一片虛擬內存上面, 這樣當**磁盤控制器**操控內核緩沖區的時候就等價于操控進程緩沖區, 整個過程避免了拷貝操作這樣做的前提是內核和用戶的緩沖區必須使用相同的頁對齊(固定的大小字節組,一般為512字節大小)具體見下圖: 3.使用分頁技術進行操作系統I/O:a.確定請求數據在文件系統的哪些磁盤區域, 這些數據可能橫跨多個文件系統, 且位置不連續b.內核空間分配足夠多的內存頁(緩沖區), 用于容納確定的文件系統頁'c.建立內存頁與磁盤文件之間的聯系, 二者建立映射d.為每一個內存頁進行檢查e.根據d操作中的檢查結果, 決定每個內存頁是否執行讀寫操作f.從磁盤中讀取文件內容, 將數據導入, 文件系統對導入數據進行解析進程與內核的緩沖區共享同一片內存區域
Java中的緩沖區(Buffer)
緩沖區基礎
- 屬性:
- 存取
Buffer內部使用get(),put()函數進行數據存儲, get/put當index位置超出范圍,拋BufferOverflowException - 轉置: 使緩沖區的內容逆置, 只需要修改position與limit指向的位置, 讓position執行末尾
- 清空緩沖區: 使用clear(), clear并沒有改變緩沖區中元素, 他所改變的就是設定了limit值, 并讓其指向0號位置
- 復制緩沖區內容:
當緩沖區屬性為只讀, 那么復制緩沖區內容就是淺復制, 對一個緩沖區的改變會反映到另外一個緩沖區上面, 新的緩沖區將會繼承舊的緩沖區所有的屬性. 當對只讀緩沖區進行put操作, 將拋ReadOnlyBufferException異常
注:
如果只讀緩沖區與可寫緩沖區共享一片內存區域, 可寫緩沖區進行改變(如put操作), 這種改變將會體現在只讀緩沖區上面; 就所謂的淺復制
字節緩沖區
- 使用字節緩沖區作為通道執行I/O操作的源和目標, 而向通道中傳遞一個非ByteBuffer對象的時候將會出現如下的問題:
上面的過程導致的問題就是嚴重性能損耗, 當非ByteBuffer對象很多的時候
通道(Channel)
Channel基本概述
通道使用ByteBuffer作為端點, 使文件系統, 進程, 網絡等等進行交互, 這種交互總是最小的開銷(通道只支持字節操作);
如下圖: FileSystem與NetWork的I/O操作是通過Channel執行
-
打開通道:
通道分為2種類型(File, Socket), File: FileChannel, Socket: SocketChannel, ServerSocketChannel, DatagramChannel;
獲得方式:
1.FileChanne fc = new FileInputStream(new File(PATH)).getChannel();
//文件有FileInputStream, FileOutputStream, RandomAccessFile
2.SocketChannel sc = SocketChannel.open();
其余Socket的Channel同2
注: 在java.net中的socket使用getChannel獲得Channel, 但這樣的Channel并不是新通道(它永運不會創建新通道), 只有存在一個與Socket關聯的通道, 這樣獲得的才是新通道, 否則就是一個假通道 -
使用通道:
通道間的數據可以是單向的也可以是雙向的, 默認的ByteChannel接口實現的是雙向數據傳輸, 但是遇到這樣的問題的時候雙向傳輸將會拋異常: FileInputStream的getChannel獲得的FileChannel對象是只讀的, 但是由于FileInputStream實現了ByteChannel接口, 因此可以調用read, write操作, 當這個管道調用write將會拋未經檢查異常NonWritableChannelException;
通道可以是阻塞, 或非阻塞; 非阻塞: 通道永遠不會讓調用線程休眠, 請求的操作立即完成, 要么返回獲得的數據, 要么返回未獲得數據;
注: 只有sockets, pipes才能使用非阻塞模式 -
關閉通道:
使用close()關閉, 關閉通道將會導致底層I/O服務線程暫時阻塞, 即使該通道處于非阻塞模式, close多次調用沒有影響(close也會阻塞, 對已經關閉的通道使用close不會產生任何操作, 只會立即返回); 關閉的時候可以使用isOpen()判斷通道開放狀態. 對于已經關閉的Channel使用讀寫都將拋CloseChannelException
注:
1.當通道實現了InterruptibleChannel接口, 那么當某一線程在該通道上阻塞并且被中斷, 那么該通道將被關閉, 被阻塞的線程拋ClosedByInterruptException(在Selectors上阻塞的中斷線程不會導致通道關閉); InterruptibleChannel的檢查手段是通過isInterrupted()判斷線程的interrupt status
看似上面這種操作過于苛刻, 線程阻塞且中斷就關閉對應的Channel, 但這完全是考慮因為操作系統而導致的I/O問題, 增強程序健壯性.
2.中斷的線程可以使用異步關閉, 實現了InterruptibleChannel的線程接口的通道可以在任何時候被關閉, 一個通道關閉的時候在這個通道上的所有阻塞的線程都將被喚醒并接受到一個AsynchronousCloseException
3.不實現InterruptibleChannel接口的通道通常不進行底層特殊操作, 這些通道永遠不會出現阻塞的問題 -
Scatter/Gather
此處的Scatter/Gather就與緩沖區基礎中讀取優化中的第2點一樣, 在多個緩沖區上實現一個I/O操作.
Scatter: 對于進程read, 從通道讀取的數據會按順序散布到多個緩沖區, 直到緩沖區或通道中數據用完.
Gather: 對于進程write, 數據從多個緩沖區按順序抽取, 然后將數據放入通道中.
下面使用Gather演示從緩沖區中獲取數據寫入管道中(Scatter與Gather相反, 就是將管道中數據存入緩沖區):
緩沖區中元素根據Position, Limit定位每個緩沖區中取到的字符串, 使用標號對應記錄緩沖區內容在Channel中順序
優點: 避免數據的來回拷貝, 可以按照不同的方式組合緩沖區數據的引用
下面給出演示代碼:
阻塞, 非阻塞; 同步, 異步簡述
-
同步, 異步, 阻塞, 非阻塞簡述:
同步和異步是相對于操作結果來說,會不會等待結果返回。
阻塞和非阻塞是相對于線程是否被阻塞。 -
這兩者存在本質的區別:
它們的修飾對象是不同的。阻塞和非阻塞是指進程訪問的數據如果尚未就緒,進程是否需要等待,簡單說這相當于函數內部的實現區別,也就是未就緒時是直接返回還是等待就緒。
而同步和異步是指訪問數據的機制,同步一般指主動請求并等待I/O操作完畢的方式,當數據就緒后在讀寫的時候必須阻塞,異步則指主動請求數據后便可以繼續處理其它任務,隨后等待I/O,操作完畢的通知,這可以使進程在數據讀寫時也不阻塞。
網絡I/O
-
I/O模型:
輸入操作包含:等待數據; 從內核向進程復制數據; 對于Socket, 先是等待數據從網絡到達, 隨后將到達的數據復制到內核的緩沖區, 最終將內核緩沖區數據復制到應用進程緩沖區 -
Unix的5種I/O模型
模型圖例如下:
- 5種IO模型比較
- IO復用:
包含select/poll/epoll, select出現最早, 然后是poll, 再是epoll
-
select與poll二者的區別:
poll提供更多的事件類型, 描述符重用率比select高一個線程對某個描述符調用select或poll, 另一個線程關閉該描述符, 會導致調用結果不確定二者的速度都慢, 每次調用都需要將全部描述符從應用進程緩沖區復制到內核緩沖區select和poll的返回結果中沒有聲明哪些描述符已經準備好了, 當返回值大于0, 采用輪詢的方式找到IO完成描述符所有系統都支持select, 只有較新的系統支持poll
select會修改描述符, poll不會, select描述符使用數組fd_set實現, 默認大小1024, 只能監聽1024個描述符, 需要修改FD_SETSIZE, poll沒有描述符數量限制 -
epoll:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
用于向內核注冊新的描述符或改變某個文件描述符的狀態, 已注冊的描述符使用紅黑樹維護, 通過回調函數內核會將IO準備好的描述符加入一個鏈表中管理
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);獲得事件完成的描述符
只需要將描述符從進程緩沖區向內核緩沖區拷貝一次, 進程不需要通過輪詢的方式來獲得事件完成的描述符
總結
以上是生活随笔為你收集整理的Java NIO_I/O基本概念_Java中的缓冲区(Buffer)_通道(Channel)_网络I/O的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++与Java中的static成员总结
- 下一篇: Java的Class类文件结构及基本字节