一、使用案例
/**
* @description: 測試buffer的讀寫
* @author:weirx
* @date:2021/11/2 13:54
* @version:3.0
*/
public class test {
public static void main(String[] args) {
try (RandomAccessFile file = new RandomAccessFile("C:\\Users\\P50\\Desktop\\text.txt", "rw")) {
FileChannel channel = file.getChannel();
//申請大小爲10的buffer
ByteBuffer buffer = ByteBuffer.allocate(10);
do {
// 向 buffer 寫入
int len = channel.read(buffer);
System.out.println("讀到字節數:" + len);
if (len == -1) {
break;
}
// 切換 buffer 讀模式
buffer.flip();
while (buffer.hasRemaining()) {
System.out.println((char) buffer.get());
}
// 切換 buffer 寫模式
buffer.clear();
} while (true);
} catch (IOException e) {
e.printStackTrace();
}
}
輸出如下:
讀到字節數:10
1
2
3
4
5
6
7
8
9
0
讀到字節數:5
a
b
c
d
e
讀到字節數:-1
根據上面的使用案例,我們可以總結出以下使用buffer的正確姿勢:
1)向 buffer 寫入數據,例如調用 channel.read(buffer)
2)調用 flip() 方法,將buffer切換至讀模式
3)調用 buffer.get()方法,從 buffer 讀取數據
4)調用 clear() 或 compact() 切換至寫模式,其中clear()會覆蓋buffer內的數據,而compact()會在原數據基礎上繼續寫
5)循環重複 1~4 的步驟
二、ByteBuffer代碼分析
在上一篇文章https://www.jianshu.com/p/994df8e0dc0e當中,已經基本說明了關於bytebuffer
的基礎內容,本章節簡單分析下其源碼,助於我們理解和學習。
看下其類圖:
從上圖看到其繼承了Buffer類,實現了Comparable接口。
2.1 Buffer類
看下buffer中有主要幾個屬性:
mark:記錄當前讀或寫的位置。
position:下一個位置。
limit:範圍。
capacity:Buffer的容量,創建時候指定,不能修改。
address:直接內存的地址。
2.1.1 Buffer讀寫模型
關於整個的讀寫模型,直接放圖了:
2.1.2 Buffer主要方法
1)構造方法:指定mark,position,limit,capacity等屬性,如果capacity小於0,或者mark比pos大的話,則拋出異常。
Buffer(int mark, int pos, int lim, int cap) { // package-private
if (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;
}
}
limit(lim)方法:指定新的範圍,如果大於容量,或範圍小於0,則拋出異常。如果當前位置大於設置的範圍,就把範圍賦值個position。如果mark大於範圍,則還原mark爲-1。倒數兩個判斷通常只會發生在,新的limit小於原limit的情況。
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;
}
position(pos)方法:如果新的postion大於limit或小於0,則從拋出異常。如果mark大於position,則還原mark爲-1。
public final Buffer position(int newPosition) {
if ((newPosition > limit) || (newPosition < 0))
throw new IllegalArgumentException();
position = newPosition;
if (mark > position) mark = -1;
return this;
}
2)Buffer提供的一些公共方法
capacity(),limit(),position()等都是獲取當前容量,範圍,位置的方法。
mark()方法:將當前的位置,手動設置一個mark標記,通常配置reset()方法使用。
public final Buffer mark() {
mark = position;
return this;
}
reset()方法:當我們使用mark()方法對position進行標記後,使用reset方法,可以將標記後的mark賦值給position。
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
flip()方法:將緩衝區切換爲讀模式,可以結合前面的圖看一下,將數據存在的最大position設置爲limit,將position設置爲0,丟棄mark。
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
rewind()方法:此方法用於,當buffer已經讀取部分數據或者全部數據後,重置buffer,使其位置重新開始,拋棄mark。
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
remaining()方法:返回當前buffer中的剩餘容量。
public final int remaining() {
return limit - position;
}
hasRemaining()方法:如果當前位置小於limit,就返回true,寫模式下表示可以繼續寫入,讀模式下表示仍有未讀取的數據;返回false時,相反。
public final boolean hasRemaining() {
return position < limit;
}
isReadOnly():是否只讀
isDirect():是否是直接內存
2.2 Comparable接口
ByteBuffer實現了Comparable接口,而在這個接口中,只有一個比較方法compareTo(T o)。
ByteBuffer實現了這個接口的方法:
public int compareTo(ByteBuffer that) {
int n = this.position() + Math.min(this.remaining(), that.remaining());
for (int i = this.position(), j = that.position(); i < n; i++, j++) {
int cmp = compare(this.get(i), that.get(j));
if (cmp != 0)
return cmp;
}
return this.remaining() - that.remaining();
}
private static int compare(byte x, byte y) {
return Byte.compare(x, y);
}
將此緩衝區與另一個緩衝區進行比較。
通過按字典順序比較它們剩餘元素的序列來比較兩個字節緩衝區,而不考慮每個序列在其相應緩衝區中的起始位置。
像通過調用Byte.compare(byte, byte)一樣比較byte元素對。
字節緩衝區無法與任何其他類型的對象進行比較。
2.3 ByteBuffer
下面來到正主ByteBuffer的源碼分析了。
2.3.1 初始化
首先來看看如何初始化一個ByteBuffer,在源碼中提供兩種方式:allocate和allocateDirect。
2.3.1.1 allocate(int capacity)
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
如上所示,最終會new一個HeapByteBuffer,堆中的buffer。跟蹤到底層其實就是我們前面分析的Buffer類。我們重點關注分配直接內存的方式。
2.3.1.2 allocateDirect(int capacity)
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
如上所示會new一個DirectByteBuffer,直接內存buffer。我們看看這個類的類圖:
如上圖所示,其繼承自MappedByteBuffer,並且實現了DirectBuffer接口。下面我們一點點分析其源碼。
我們申請直接內存時候,會通過父類的構造,而其正是一個MappedByteBuffer。不停的使用其父類的構造方法,當然最終,也是一個ByteBuffer。
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
... ...
}
MappedByteBuffer(int mark, int pos, int lim, int cap) { // package-private
super(mark, pos, lim, cap);
this.fd = null;
}
ByteBuffer(int mark, int pos, int lim, int cap) { // package-private
this(mark, pos, lim, cap, null, 0);
}
什麼是MappedByteBuffer?首先看下這個類的註釋說明:
直接字節緩衝區,其內容是文件的內存映射區域。
映射字節緩衝區是通過FileChannel.map方法創建的。 此類使用特定於內存映射文件區域的操作擴展了ByteBuffer類。
映射的字節緩衝區及其表示的文件映射在緩衝區本身被垃圾收集之前一直有效。
映射字節緩衝區的內容可以隨時更改,例如,如果該程序或其他程序更改了映射文件的相應區域的內容。 此類更改是否發生以及何時發生取決於操作系統,因此未指定。
映射字節緩衝區的全部或部分可能隨時無法訪問,例如,如果映射文件被截斷。
強烈建議採取適當的預防措施,以避免該程序或同時運行的程序對映射文件的操作,但讀取或寫入文件的內容除外。
映射字節緩衝區的其他行爲與普通的直接字節緩衝區沒有什麼不同。
下面繼續跟蹤直接內存的構造方法:
DirectByteBuffer(int cap) { // package-private
// 構造一個MappedByteBuffer
super(-1, 0, cap, cap);
// 內存是否按照頁分配對齊
boolean pa = VM.isDirectMemoryPageAligned();
// 初始化頁的大小
int ps = Bits.pageSize();
// 如果內存是與頁分配對其,則大小是指定cap+默認頁的大小,否則就是cap
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
// 這個是計算分配直接內存大小的方法,這個方法裏面允許我們使用-XX:MaxDirectMemorySize參數指定堆外內存的最大值
Bits.reserveMemory(size, cap);
long base = 0;
try {
// 真正分配堆外內存,如果內存不足會拋出OOM異常
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
// 釋放內存
Bits.unreserveMemory(size, cap);
throw x;
}
// 保存一下分配的堆外內存值
unsafe.setMemory(base, size, (byte) 0);
//如果內存是按照頁對其分配,並且分配的內存除以每個頁的大小能夠整除沒有餘數,計算直接內存的地址
if (pa && (base % ps != 0)) {
// 四捨五入到頁的邊界
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 創建一個處理直接內存的清理守護線程,這是一個PhantomReference虛引用,通過監測虛引用可以做一些善後操作
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
2.3.2 ByteBuffer寫入
通常有兩種方法寫入數據:
1)調用 channel 的 read 方法
int readBytes = channel.read(buf);
2)調用 buffer 自己的 put 方法
如上圖所示,支持給中類型的寫入,以及指定index位置的寫入等等,此處不一一舉例了。
2.3.2 ByteBuffer讀取
讀取數據同樣有兩種方式:
1)調用 channel 的 write 方法
int writeBytes = channel.write(buf);
2)調用 buffer 自己的 get 方法
如上圖所示同樣支持各種類型的讀取,以及指定範圍的讀取。
注意:
- get()方法會讓buffer的position向後移動,如果想要讀取重複數據,可使用rewind()重置position。
- get(int) 指定索引位置的方法不會使得position向後移動。
其他的方法與Buffer是相同的,前面已經都講解過了。
三、ByteBuffer與字符串的相互轉換
public class StringToBuffer {
public static void main(String[] args) {
//字符串轉byteBuffer
ByteBuffer buffer1 = StandardCharsets.UTF_8.encode("你好");
System.out.println(buffer1);
//byteBuffer裝字符串
CharBuffer buffer2 = StandardCharsets.UTF_8.decode(buffer1);
System.out.println(buffer2.getClass());
System.out.println(buffer2.toString());
}
}
四、分散讀寫(多個Buffer同時讀取或寫入)
有一個文件叫做text.txt,內容如下:
onetwothree
4.1 分散讀:
實例代碼如下:
public class ScatteringRead {
public static void main(String[] args) {
try (RandomAccessFile file = new RandomAccessFile("C:\\Users\\P50\\Desktop\\text.txt", "rw")) {
FileChannel channel = file.getChannel();
ByteBuffer a = ByteBuffer.allocate(3);
ByteBuffer b = ByteBuffer.allocate(3);
ByteBuffer c = ByteBuffer.allocate(5);
channel.read(new ByteBuffer[]{a, b, c});
a.flip();
b.flip();
c.flip();
System.out.println(print(a));
System.out.println(print(b));
System.out.println(print(c));
} catch (IOException e) {
e.printStackTrace();
}
}
static String print(ByteBuffer b){
StringBuilder stringBuilder = new StringBuilder();
for(int i =0 ; i< b.limit();i++){
stringBuilder.append((char)b.get(i));
}
return stringBuilder.toString();
}
}
結果:
one
two
three
4.2 分散寫
public class GatheringWrite {
public static void main(String[] args) throws Exception {
RandomAccessFile file = new RandomAccessFile("C:\\Users\\P50\\Desktop\\text.txt", "rw");
FileChannel channel = file.getChannel();
ByteBuffer d = ByteBuffer.allocate(4);
ByteBuffer e = ByteBuffer.allocate(4);
channel.position(11);
d.put(new byte[]{'f', 'o', 'u', 'r'});
e.put(new byte[]{'f', 'i', 'v', 'e'});
d.flip();
e.flip();
System.out.println(print(d));
System.out.println(print(e));
channel.write(new ByteBuffer[]{d, e});
}
static String print(ByteBuffer b) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < b.limit(); i++) {
stringBuilder.append((char) b.get(i));
}
return stringBuilder.toString();
}
}
結果:
four
five
文件內容如下所示:
onetwothreefourfive
注意
Buffer是非線程安全的。