自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

讓我們一起玩轉(zhuǎn) ByteBuffer

開發(fā) 前端
Buffer 存在的目的是為了減少與設(shè)備(例如磁盤)的交互頻率,在之前的博客中也提到過「磁盤的讀寫是很昂貴的操作」。

[[442736]]

本文轉(zhuǎn)載自微信公眾號「SH的全棧筆記」,作者SH的全棧筆記。轉(zhuǎn)載本文請聯(lián)系SH的全棧筆記公眾號。

為什么要講 Buffer

首先為什么一個小小的 Buffer 我們需要單獨拎出來聊?或者說,Buffer 具體是在哪些地方被用到的呢?

例如,我們從磁盤上讀取一個文件,并不是直接就從磁盤加載到內(nèi)存中,而是首先會將磁盤中的數(shù)據(jù)復(fù)制到內(nèi)核緩沖區(qū)中,然后再將數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到用戶緩沖區(qū)內(nèi),在圖里看起來就是這樣:

從磁盤讀取文件

再比如,我們往磁盤上寫文件,也不是直接將數(shù)據(jù)寫到磁盤。而是將數(shù)據(jù)從用戶緩沖區(qū)寫到內(nèi)核緩沖區(qū),由操作系統(tǒng)擇機(jī)將其刷入磁盤,圖跟上面這個差不多,就不畫了,自行理解。

再再比如,服務(wù)器接受客戶端發(fā)過來的數(shù)據(jù)時,也不是直接到用戶態(tài)的 Buffer 中。而是會先從網(wǎng)卡到內(nèi)核態(tài)的 Buffer 中,再從內(nèi)核態(tài)的 Buffer 中復(fù)制到用戶態(tài)的 Buffer 中。

那為什么要這么麻煩呢?復(fù)制來復(fù)制去的,首先我們用排除法排除這樣做是為了好玩。

Buffer 存在的目的是為了減少與設(shè)備(例如磁盤)的交互頻率,在之前的博客中也提到過「磁盤的讀寫是很昂貴的操作」。那昂貴在哪里呢?簡單來說,和設(shè)備的交互(例如和磁盤的IO)會設(shè)計到操作系統(tǒng)的中斷。中斷需要保存之前的進(jìn)程運行的上下文,中斷結(jié)束之后又需要恢復(fù)這個上下文,并且還涉及到內(nèi)核態(tài)和用戶態(tài)的切換,總體上是個耗時的操作。

看到這里,不熟悉操作系統(tǒng)的話可能會有點疑惑。例如:

  • 啥是用戶態(tài)
  • 啥是內(nèi)核態(tài)

大家可以去看看我之前寫的文章 《簡單聊聊用戶態(tài)和內(nèi)核態(tài)的區(qū)別》

Buffer 的使用

我們通過 Java 中 NIO 包中實現(xiàn)的 Buffer 來給大家講解,Buffer 總共有 7 種實現(xiàn),就包含了 Java 中實現(xiàn)的所有數(shù)據(jù)類型。

Buffer的種類 (1)

本篇文章中,我們使用的是 ByteBuffer,其常用的方法都有:

  • put
  • get
  • flip
  • rewind
  • mark
  • reset
  • clear

接下來我們就通過實際的例子來了解這些方法。

put

put 就是往 ByteBuffer 里寫入數(shù)據(jù),其有有很多重載的實現(xiàn):

  1. public ByteBuffer put(ByteBuffer src) {...} 
  2.  
  3. public ByteBuffer put(byte[] src, int offset, int length) {...} 
  4.  
  5. public final ByteBuffer put(byte[] src) {...} 

我們可以直接傳入 ByteBuffer 對象,也可以直接傳入原生的 byte 數(shù)組,還可以指定寫入的 offset 和長度等等。接下來看個具體的例子:

  1. public static void main(String[] args) { 
  2.     ByteBuffer buffer = ByteBuffer.allocate(16); 
  3.     buffer.put(new byte[]{'s','h'}); 

為了能讓大家更直觀的看出 ByteBuffer 內(nèi)部的情況,我將它整理成了圖的形式。當(dāng)上面的代碼運行完之后 buffer 的內(nèi)部長這樣:

put

當(dāng)你嘗試使用 System.out.println(buffer) 去打印變量 buffer 的時候,你會看到這樣的結(jié)果:

  1. java.nio.HeapByteBuffer[pos=2 lim=16 cap=16] 

圖里、控制臺里都有 position 和 limit 變量,capacity 大家能理解,就是我們創(chuàng)建這個 ByteBuffer 的制定的大小 16。

而至于另外兩個變量,相信大家從圖中也可以看出來,position 變量指向的是下一次要寫入的下標(biāo),上面的代碼我們只寫入了 2 個字節(jié),所以 position 指向的是 2,而這個 limit 就比較有意思了,這個在后面的使用中結(jié)合例子一起講。

get

get 是從 ByteBuffer 中獲取數(shù)據(jù)。

  1. public static void main(String[] args) { 
  2.   ByteBuffer buffer = ByteBuffer.allocate(16); 
  3.   buffer.put(new byte[]{'s','h'}); 
  4.   System.out.println(buffer.get()); 

如果你運行完上面的代碼你會發(fā)現(xiàn),打印出來的結(jié)果是 0 ,并不是我們期望的 s 的 ASCII 碼 115。

首先告訴大家結(jié)論,這是符合預(yù)期的,這個時候就不應(yīng)該能獲取到值。我們來看看 get 的源碼:

  1. public byte get() { return hb[ix(nextGetIndex())]; } 
  2.  
  3. protected int ix(int i) { return i + offset; } 
  4.  
  5. final int nextGetIndex() {                           
  6.   int p = position; 
  7.   if (p >= limit) 
  8.     throw new BufferUnderflowException(); 
  9.   // 這里 position 會往后移動一位 
  10.   position = p + 1; 
  11.   return p; 

當(dāng)前 position 是 2,而 limit 是 16,所以最終 nextGetIndex 計算出來的值就是變量 p 的值 2 ,再過一次 ix ,那就是 2 + 0 = 2,這里的 offset 的值默認(rèn)為 0 。

所以簡單來說,最終會取到下標(biāo)為 2 的數(shù)據(jù),也就是下圖這樣。

所以我們當(dāng)然獲取不到數(shù)據(jù)。但是這里需要關(guān)注的是,調(diào)用 get 方法雖然沒有獲取到任何數(shù)據(jù),但是會使得 position 指針往后移動。換句話說,會占用一個位置。如果連續(xù)調(diào)用幾次這種 get 之后,再調(diào)用 put 方法寫入數(shù)據(jù),就會造成有幾個位置沒有賦值。舉個例子,假設(shè)我們運行以下代碼:

  1. public static void main(String[] args) { 
  2.   ByteBuffer buffer = ByteBuffer.allocate(16); 
  3.   buffer.put(new byte[]{'s','h'}); 
  4.  
  5.   buffer.get(); 
  6.   buffer.get(); 
  7.   buffer.get(); 
  8.   buffer.get(); 
  9.  
  10.   buffer.put(new byte[]{'e'}); 

數(shù)據(jù)就會變成下圖這樣,position 會往后移動

那你可能會問,那我真的需要獲取數(shù)據(jù)咋辦?在這種情況下,可以像這樣獲?。?/p>

  1. public static void main(String[] args) { 
  2.   ByteBuffer buffer = ByteBuffer.allocate(16); 
  3.   buffer.put(new byte[]{'s'}); 
  4.   System.out.println(buffer.get(0)); // 115 

傳入我們想要獲取的下標(biāo),就可以直接獲取到,并且不會造成 position 的后移。

看到這那你更懵逼了,合著 get() 就沒法用唄?還必須要給個 index。這就需要聊一下另一個方法 flip了。

flip

廢話不多說,先看看例子:

  1. public static void main(String[] args) { 
  2.   ByteBuffer buffer = ByteBuffer.allocate(16); 
  3.   buffer.put(new byte[]{'s''h'}); // java.nio.HeapByteBuffer[pos=2 lim=16 cap=16] 
  4.   buffer.flip(); 
  5.   System.out.println(buffer); // java.nio.HeapByteBuffer[pos=0 lim=2 cap=16] 

有意思的事情發(fā)生了,調(diào)用了 flip 之后,position 從 2 變成了 0,limit 從 16 變成了 2。

這個單詞是「翻動」的意思,我個人的理解是像翻東西一樣把之前存的東西全部翻一遍

你會發(fā)現(xiàn),position 變成了 0,而 limit 變成 2,這個范圍剛好是有值的區(qū)間。

接下來就更有意思了:

  1. public static void main(String[] args) { 
  2.   ByteBuffer buffer = ByteBuffer.allocate(16); 
  3.   buffer.put(new byte[]{'s''h'}); 
  4.   buffer.flip(); 
  5.   System.out.println((char)buffer.get()); // s 
  6.   System.out.println((char)buffer.get()); // h 

調(diào)用了 flip 之后,之前沒法用的 get() 居然能用了。結(jié)合 get 中給的源碼不難分析出來,由于 position 變成了 0,最終計算出來的結(jié)果就是 0,同時使 position 向后移動一位。

終于到這了,你可以理解成 Buffer 有兩種狀態(tài),分別是:

  • 讀模式
  • 寫模式

剛剛創(chuàng)建出來的 ByteBuffer 就處于一個寫模式的狀態(tài),通過調(diào)用 flip 我們可以將 ByteBuffer 切換成讀模式。但需要注意,這里講的讀、寫模式只是一個邏輯上的概念。

舉個例子,當(dāng)調(diào)用 flip 切換到所謂的寫模式之后,依然能夠調(diào)用 put 方法向 ByteBuffer 中寫入數(shù)據(jù)。

  1. public static void main(String[] args) { 
  2.   ByteBuffer buffer = ByteBuffer.allocate(16); 
  3.   buffer.put(new byte[]{'s''h'}); 
  4.   buffer.flip(); 
  5.   buffer.put(new byte[]{'e'}); 

這里的 put 操作依然能成功,但你會發(fā)現(xiàn)最后寫入的 e 覆蓋了之前的數(shù)據(jù),現(xiàn)在 ByteBuffer 的值變成了 eh 而不是 sh 了。

flip_put

所以你現(xiàn)在應(yīng)該能夠明白,讀模式、寫模式更多的含義應(yīng)該是:

  • 方便你讀的模式
  • 方便你寫的模式

順帶一提,調(diào)用 flip 進(jìn)入寫讀模式之后,后續(xù)如果調(diào)用 get() 導(dǎo)致 position 大于等于了 limit 的值,程序會拋出 BufferUnderflowException 異常。這點從之前 get 的源碼也可以看出來。

rewind

rewind 你也可以理解成是運行在讀模式下的命令,給大家看個例子:

  1. public static void main(String[] args) { 
  2.   ByteBuffer buffer = ByteBuffer.allocate(16); 
  3.   buffer.put(new byte[]{'s''h'}); 
  4.   buffer.flip(); 
  5.   System.out.println((char)buffer.get()); // s 
  6.   System.out.println((char)buffer.get()); // h 
  7.  
  8.   // 從頭開始讀 
  9.   buffer.rewind(); 
  10.  
  11.   System.out.println((char)buffer.get()); // s 
  12.   System.out.println((char)buffer.get()); // h 

所謂的從頭開始讀就是把 position 給歸位到下標(biāo)為 0 的位置,其源碼也很簡單:

  1. public final Buffer rewind() { 
  2.   position = 0; 
  3.   mark = -1; 
  4.   return this; 

rewind

就是簡單的把 position 賦值為 0,把 mark 賦值為 -1。那這個 mark 又是啥東西?這就是我們下一個要聊的方法。

  1. public static void main(String[] args) { 
  2.   ByteBuffer buffer = ByteBuffer.allocate(16); 
  3.   buffer.put(new byte[]{'a''b''c''d'}); 
  4.    
  5.   // 切換到讀模式 
  6.   buffer.flip(); 
  7.   System.out.println((char) buffer.get()); // a 
  8.   System.out.println((char) buffer.get()); // b 
  9.  
  10.   // 控記住當(dāng)前的 position 
  11.   buffer.mark(); 
  12.    
  13.   System.out.println((char) buffer.get()); // c 
  14.   System.out.println((char) buffer.get()); // d 
  15.  
  16.   // 將 position reset 到 mark 的位置 
  17.   buffer.reset(); 
  18.   System.out.println((char) buffer.get()); // c 
  19.   System.out.println((char) buffer.get()); // d 

可以看到的是 ,我們在 position 等于 2 的時候,調(diào)用了 mark 記住了 position 的位置。然后遍歷完了所有的數(shù)據(jù)。然后調(diào)用 reset 使得 position 回到了 2 的位置,我們繼續(xù)調(diào)用 get ,c d 就又可以被打印出來了。

clear

clear 表面意思看起來是將 buffer 清空的意思,但其實不是,看這個:

  1. public static void main(String[] args) { 
  2.   ByteBuffer buffer = ByteBuffer.allocate(16); 
  3.   buffer.put(new byte[]{'a''b''c''d'}); 

put 完之后,buffer 的情況是這樣的。

當(dāng)我們調(diào)用完 clear 之后,buffer 就會變成這樣。

所以,你可以理解為,調(diào)用 clear 之后只是切換到了寫模式,因為這個時候往里面寫數(shù)據(jù),會覆蓋之前寫的數(shù)據(jù),相當(dāng)于起到了 clear 作用,再舉個例子:

  1. public static void main(String[] args) { 
  2.   ByteBuffer buffer = ByteBuffer.allocate(16); 
  3.   buffer.put(new byte[]{'a''b''c''d'}); 
  4.   buffer.clear(); 
  5.   buffer.put(new byte[]{'s','h'}); 

可以看到,運行完之后 buffer 的數(shù)據(jù)變成了 shcd,后寫入的數(shù)據(jù)將之前的數(shù)據(jù)給覆蓋掉了。

除了 clear 可以切換到寫模式之外,還有另一個方法可以切換,這就是本篇要講的最后一個方法 compact。

compact

先一句話給出 compact 的作用:將還沒有讀完的數(shù)據(jù)挪到 Buffer 的首部,并切換到寫模式,代碼如下:

  1. public static void main(String[] args) { 
  2.   ByteBuffer buffer = ByteBuffer.allocate(16); 
  3.   buffer.put("abcd".getBytes(StandardCharsets.UTF_8)); 
  4.  
  5.   // 切換到讀模式 
  6.   buffer.flip(); 
  7.   System.out.println((char) buffer.get()); // a 
  8.  
  9.   // 將沒讀過的數(shù)據(jù), 移到 buffer 的首部 
  10.   buffer.compact(); // 此時 buffer 的數(shù)據(jù)就會變成 bcdd 

當(dāng)運行完 flip 之后,buffer 的狀態(tài)應(yīng)該沒什么問題了:

運行完 flip 之后

而 compact 之后發(fā)生了什么呢?簡單來說就兩件事:

  • 將 position 移動至對應(yīng)的位置
  • 將沒有讀過的數(shù)據(jù)移動到 buffer 的首部

這個對應(yīng)是啥呢?先給大家舉例子;例如沒有讀的數(shù)據(jù)是 bcd,那么 position 就為 3;如果沒有讀的數(shù)據(jù)為 cd,position 就為 2。所以你發(fā)現(xiàn)了,position 的值為沒有讀過的數(shù)據(jù)的長度。

從 buffer 內(nèi)部實現(xiàn)機(jī)制來看,凡是在 position - limit 這個區(qū)間內(nèi)的,都算沒有讀過的數(shù)據(jù)

所以,當(dāng)運行完 compact 之后,buffer 長這樣:

運行完 compact 之后

limit 為 16 是因為 compact 使 buffer 進(jìn)入了所謂的寫模式。

EOF

還有一些其他的方法就不在這里列舉了,大家感興趣可以自己去玩玩,都沒什么理解上的難度了。之后可能會再專門寫一寫 Channel 和 Selector,畢竟 Java 的 nio 三劍客,感興趣的可以關(guān)注一下。

 

責(zé)任編輯:武曉燕 來源: SH的全棧筆記
相關(guān)推薦

2022-03-31 18:59:43

數(shù)據(jù)庫InnoDBMySQL

2021-08-27 07:06:10

IOJava抽象

2022-03-08 17:52:58

TCP格式IP

2021-07-15 07:23:28

Singlefligh設(shè)計

2021-11-26 07:00:05

反轉(zhuǎn)整數(shù)數(shù)字

2022-06-26 09:40:55

Django框架服務(wù)

2022-02-14 07:03:31

網(wǎng)站安全MFA

2016-09-06 10:39:30

Dell Techno

2022-02-14 10:16:22

Axios接口HTTP

2022-01-17 06:59:40

Grep指令linux

2023-08-14 08:38:26

反射reflect結(jié)構(gòu)體

2022-07-10 23:15:46

Go語言內(nèi)存

2023-08-02 08:35:54

文件操作數(shù)據(jù)源

2022-08-01 07:57:03

數(shù)組操作內(nèi)存

2021-07-31 11:40:55

Openresty開源

2012-04-14 20:47:45

Android

2021-12-16 12:01:21

區(qū)塊鏈Libra貨幣

2022-09-26 14:25:55

Flowable流程ID

2021-11-09 23:54:19

開發(fā)SMI Linkerd

2014-02-25 08:59:14

點贊
收藏

51CTO技術(shù)棧公眾號