Java NIO之缓冲区
1.簡介
Java NIO 相關類在 JDK 1.4 中被引入,用于提高 I/O 的效率。Java NIO 包含了很多東西,但核心的東西不外乎 Buffer、Channel 和 Selector。本文中,我們先來聊聊的 Buffer 的實現(xiàn)。Channel 和 Selector 將在隨后的文章中講到。
?2.繼承體系
Buffer 的繼承類比較多,用于存儲各種類型的數(shù)據(jù)。包括 ByteBuffer、CharBuffer、IntBuffer、FloatBuffer 等等。這其中,ByteBuffer 最為常用。所以接下來將會主要分析 ByteBuffer 的實現(xiàn)。Buffer 的繼承體系圖如下:
?3.屬性及相關操作
Buffer 本質就是一個數(shù)組,只不過在數(shù)組的基礎上進行適當?shù)姆庋b,方便使用。 Buffer 中有幾個重要的屬性,通過這幾個屬性來顯示數(shù)據(jù)存儲的信息。這個屬性分別是:
| capacity?容量 | Buffer 所能容納數(shù)據(jù)元素的最大數(shù)量,也就是底層數(shù)組的容量值。在創(chuàng)建時被指定,不可更改。 |
| position 位置 | 下一個被讀或被寫的位置 |
| limit 上界 | 可供讀寫的最大位置,用于限制?position,position < limit |
| mark 標記 | 位置標記,用于記錄某一次的讀寫位置,可以通過 reset 重新回到這個位置 |
?3.1 ByteBuffer 初始化
ByteBuffer 可通過 allocate、allocateDirect 和 wrap 等方法初始化,這里以 allocate 為例:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public static ByteBuffer allocate(int capacity) {if (capacity < 0)throw new IllegalArgumentException();return new HeapByteBuffer(capacity, capacity); }HeapByteBuffer(int cap, int lim) {super(-1, 0, lim, cap, new byte[cap], 0); }ByteBuffer(int mark, int pos, int lim, int cap, byte[] hb, int offset) {super(mark, pos, lim, cap);this.hb = hb;this.offset = offset; } |
上面是 allocate 創(chuàng)建 ByteBuffer 的過程,ByteBuffer 是抽象類,所以實際上創(chuàng)建的是其子類 HeapByteBuffer。HeapByteBuffer 在構造方法里調用父類構造方法,將一些參數(shù)值傳遞給父類。最后父類再做一次中轉,相關參數(shù)最終被傳送到 Buffer 的構造方法中了。我們再來看一下 Buffer 的源碼:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public abstract class Buffer {// Invariants: mark <= position <= limit <= capacityprivate int mark = -1;private int position = 0;private int limit;private int capacity;Buffer(int mark, int pos, int lim, int cap) { // package-privateif (cap < 0)throw new IllegalArgumentException("Negative capacity: " + cap);this.capacity = cap;limit(lim);position(pos);if (mark >= 0) {if (mark > pos)throw new IllegalArgumentException("mark > position: ("+ mark + " > " + pos + ")");this.mark = mark;}} } |
Buffer 創(chuàng)建完成后,底層數(shù)組的結構信息如下:
上面的幾個屬性作為公共屬性,被放在了 Buffer 中,相關的操作方法也是封裝在 Buffer 中。那么接下來,我們來看看這些方法吧。
?3.2 ByteBuffer 讀寫操作
ByteBuffer 讀寫操作時通過 get 和 put 完成的,這兩個方法都有重載,我們只看其中一個。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // 讀操作 public byte get() {return hb[ix(nextGetIndex())]; }final int nextGetIndex() {if (position >= limit)throw new BufferUnderflowException();return position++; }// 寫操作 public ByteBuffer put(byte x) {hb[ix(nextPutIndex())] = x;return this; }final int nextPutIndex() {if (position >= limit)throw new BufferOverflowException();return position++; } |
讀寫操作都會修改 position 的值,每次讀寫的位置是當前 position 的下一個位置。通過修改 position,我們可以讀取指定位置的數(shù)據(jù)。當然,前提是 position < limit。Buffer 中提供了position(int)?方法用于修改 position 的值。
| 1 2 3 4 5 6 7 | public final Buffer position(int newPosition) {if ((newPosition > limit) || (newPosition < 0))throw new IllegalArgumentException();position = newPosition;if (mark > position) mark = -1;return this; } |
當我們向一個剛初始化好的 Buffer 中寫入一些數(shù)據(jù)時,數(shù)據(jù)存儲示意圖如下:
如果我們想讀取剛剛寫入的數(shù)據(jù),就需要修改 position 的值。否則 position 將指向沒有存儲數(shù)據(jù)的空間上,讀取空白空間是沒意義的。如上圖,我們可以將 position 設置為 0,這樣就能從頭讀取剛剛寫入的數(shù)據(jù)。
僅修改 position 的值是不夠的,如果想正確讀取剛剛寫入的數(shù)據(jù),還需修改 limit 的值,不然還是會讀取到空白空間上的內容。我們將 limit 指向數(shù)據(jù)區(qū)域的尾部,即可避免這個問題。修改 limit 的值通過 limit(int) 方法進行。
| 1 2 3 4 5 6 7 8 | public final Buffer limit(int newLimit) {if ((newLimit > capacity) || (newLimit < 0))throw new IllegalArgumentException();limit = newLimit;if (position > limit) position = limit;if (mark > limit) mark = -1;return this; } |
修改后,數(shù)據(jù)存儲示意圖如下:
上面為了正確讀取寫入的數(shù)據(jù),需要兩步操作。Buffer 中提供了一個便利的方法,將這兩步操作合二為一,即 flip 方法。
| 1 2 3 4 5 6 7 8 | public final Buffer flip() {// 1. 設置 limit 為當前位置limit = position;// 1. 設置 position 為0position = 0;mark = -1;return this; } |
?3.3 ByteBuffer 標記
我們在讀取或寫入的過程中,可以在感興趣的位置打上一個標記,這樣我們可以通過這個標記再次回到這個位置。Buffer 中,打標記的方法是 mark,回到標記位置的方法時 reset。簡單看下源碼吧。
| 1 2 3 4 5 6 7 8 9 10 11 12 | public final Buffer mark() {mark = position;return this; }public final Buffer reset() {int m = mark;if (m < 0)throw new InvalidMarkException();position = m;return this; } |
打標記及回到標記位置的流程如下:
?4.DirectByteBuffer
在 ByteBuffer 初始化一節(jié)中,我介紹了 ByteBuffer 的 allocate 方法,該方法實際上創(chuàng)建的是 HeapByteBuffer 對象。除了 allocate 方法,ByteBuffer 還有一個方法 allocateDirect。這個方法創(chuàng)建的是 DirectByteBuffer 對象。兩者有什么區(qū)別呢?簡單的說,allocate 方法所請求的空間是在 JVM 堆內進行分配的,而 allocateDirect 請求的空間則是在 JVM 堆外的,這部分空間不被 JVM 所管理。那么堆內空間和堆空間在使用上有什么不同呢?用一個表格列舉一下吧。
| 堆內空間 | 分配速度快 | JVM 整理內存空間時,堆內空間的位置會被搬動,比較笨重 |
| 堆外空間 | 1. 空間位置固定,不用擔心空間被 JVM 隨意搬動 2. 降低堆內空間的使用率 | 1. 分配速度慢 2. 回收策略比較復雜 |
DirectByteBuffer 牽涉的底層技術點比較多,想要弄懂,還需要好好打基礎才行。由于本人目前能力很有限,關于 DirectByteBuffer 只能簡單講講。待后續(xù)能力提高時,我會再來重寫這部分的內容。如果想了解這方面的內容,建議大家看看其他的文章。
?5.總結
Buffer 是 Java NIO 中一個重要的輔助類,使用比較頻繁。在不熟悉 Buffer 的情況下,有時候很容易因為忘記調用 flip 或其他方法導致程序出錯。不過好在 Buffer 的源碼不難理解,大家可以自己看看,這樣可以避免出現(xiàn)一些奇怪的錯誤。
好了,本文到這里就結束了,謝謝閱讀!
- 本文鏈接:?https://www.tianxiaobo.com/2018/03/04/Java-NIO之緩沖區(qū)/
from:?http://www.tianxiaobo.com/2018/03/04/Java-NIO%E4%B9%8B%E7%BC%93%E5%86%B2%E5%8C%BA/?
總結
以上是生活随笔為你收集整理的Java NIO之缓冲区的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于epoll实现简单的web服务器
- 下一篇: JAVA NIO之文件通道