1. 程式人生 > 實用技巧 >NIO之路2--Java中NIO原始碼解析

NIO之路2--Java中NIO原始碼解析

一、IO多路複用

傳統的BIO伺服器處理客戶端IO請求時會為每一個客戶端請求都分配一個執行緒去處理,當客戶端數量增加時會導致服務端執行緒數過多而帶來效能隱患,所以迫不得已需要一個執行緒處理多個客戶端請求,也就衍生了多路複用IO模型,Java中的NIO核心就是使用到了作業系統的多路複用IO。

IO多路複用的本質是核心緩衝IO資料,應用程式開啟執行緒監控多個檔案描述符,一個IO連結對於一個檔案描述符,一旦某個檔案描述符就緒,會通知應用程式執行對應的讀操作或者寫操作。

作業系統提供了三種多路複用API給應用程式使用,分別是select、poll和epoll

1.1、select機制

select函式有一個引數為fd_set,表示監控的描述符集合,資料結構為long型別陣列,每一個數組元素都與一個開啟的檔案控制代碼相關聯,檔案控制代碼可以是socket描述符、檔案描述符等;當呼叫select函式之後,核心根據IO的狀態更新fd_set的值,由此來通知呼叫select函式的應用程式那個IO就緒了。從流程上看select和同步阻塞IO差不多,而且select還額外多了select操作,但是select的優勢是可以同時監控多個IO操作,而同步阻塞IO想要做到同時監控多個IO操作必須採用多執行緒的方式。很明顯從執行緒消耗上來說,selec更合適一個執行緒同時和多個IO操作互動的場景。

select的問題:

1、每次呼叫select,都需要將fd_set從使用者空間複製到核心空間,當fd_set比較大時,複製消耗較大

2、每次呼叫select,核心都需要遍歷一次fd_set,當fd_set比較大時,遍歷消耗較大

3、核心對於fd_set作了大小限制,最大值為1024或2048

4、當描述符數量較多,而活躍的較少時效能浪費較大,甚至還不如同步阻塞IO模型

1.2、poll機制

poll機制的實現邏輯和select基本上一致,只是沒有對描述符的數量做限制,儲存描述符的資料結構為連結串列結構poll相當於是select的改良吧,但是也只是在同時監控的描述符數量上改良了,其他實現邏輯並沒有變,所以還是會有select遇到的問題select和poll都只適合檔案描述符數量和活躍數都多時,如果檔案描述符數量較多而活躍的較少時並不適合。

1.3、epoll機制

select和poll機制最大的問題是每次呼叫函式都需要將檔案描述符集合從使用者空間複製到核心空間,另外每次都需要線性遍歷所有的檔案描述符判斷狀態。而epoll的實現方式是通過註冊事件回撥通知的方式實現,另外通過一個檔案描述符來管控多個檔案描述符,同樣也沒有檔案描述符數量的上限。epoll的實現流程為通過呼叫核心的epoll_ctl函式註冊需要監聽的檔案描述符以及對應的事件,核心一旦發現該檔案描述符狀態就緒,就會將所有的已經就緒的檔案描述符儲存到ready集合中,應用程式通過函式epoll_wait函式不停從ready集合中獲取已經就緒的檔案描述符即可,所以epoll不需要對所有的檔案描述符進行遍歷,而只需要對已經就緒的檔案描述符進行遍歷即可。

當然如果檔案描述符全部是活躍狀態的,那麼epoll機制的效能可能還沒有select和poll的高,但是大多數情況下都是檔案描述符數量較多,而活躍數較少的場景。

另外select、poll和epoll處理IO事件時都預設是水平觸發,也就是每次查下檔案描述符狀態都是獲取到所有就緒的檔案描述符,如果對於就緒的檔案描述符不進行處理,那麼每次呼叫時都會返回該檔案描述符;

而epoll除了支援水平觸發之外,還支援邊緣觸發模式,邊緣觸發模式是每次只會返回上一次呼叫之後到目前為止的就緒檔案描述符,也就是說一個檔案描述符就緒事件只會通過epoll_wait方法返回一次,所以需要應用程式立即處理,如果不處理那麼就需要等到下一次檔案描述符就緒。邊緣觸發模式相比於水平觸發模式來說大量減少了就緒事件觸發的次數,所以效率更高,但是需要應用程式快取事件就緒狀態並且立即處理,否則可能會丟失資料。

總結select、poll、epoll的對比

select poll epoll
獲取FD狀態的方式 線性遍歷所有FD 線性遍歷所有的FD 註冊FD就緒的事件,回撥通知
FD數量限制 1024或2048 無上限 無上限
FD儲存資料結構 陣列 連結串列 紅黑樹
IO效率 線性遍歷,時間複雜度o(n) 線性遍歷,時間複雜度o(n) 遍歷已經就緒的事件集合,時間複雜度o(1)
FD複製到核心 每次呼叫都需要複製一次 每次呼叫都需要複製一次 epoll_ctl註冊時複製一次,epoll_wait不需要複製
IO觸發模式 僅支援水平觸發 僅支援水平觸發 支援水平觸發和邊緣觸發

二、NIO理論淺析

BIO:同步阻塞IO,伺服器會為每個IO連線分配一個執行緒處理IO操作,如果沒有IO操作,那麼執行緒就一直處於阻塞狀態,直到能夠讀寫IO資料操作。

BIO的弊端:

1、客戶端連線數和伺服器執行緒數1:1,隨著客戶端數量增加,伺服器會有較大的建立銷燬執行緒的效能消耗

2、IO操作的執行緒使用率較低,因為大部分場景下客戶端連線之後並不是一直處於IO操作狀態,所以大部分情況下會導致執行緒處於空閒狀態

NIO:同步非阻塞IO,對於客戶端的連線伺服器並不會立即分配執行緒處理IO操作,而是先進行註冊,註冊每個客戶端連線以及客戶端需要監聽的事件型別,一旦事件就緒(如可讀事件、可寫事件)那麼才會通過伺服器分配執行緒處理具體的IO操作

NIO相對於BIO的優點:

1、只需要一個註冊執行緒就可以管理所有的客戶端註冊連結操作

2、只有客戶端存在有效的IO操作時伺服器才會分配執行緒去處理,大幅度提高執行緒的使用率

3、伺服器採用採用輪詢的方式監聽客戶端連結的事件就緒狀態,而具體的IO操作執行緒不會被阻塞

2.1、NIO的三大核心

提到NIO就不得不先了解NIO相比於BIO的三大核心模組,分別是多路複用選擇器(Selector)、緩衝區(Buffer)和通道(Channel)

2.1.1、Channel(通道)

Channel可以理解為是通訊管道,客戶端和服務端之前通過channel互相傳送資料。通常情況下流的讀寫是單向的,從傳送端到接收端。而通道支援雙向同時通訊,客戶端和服務端可以同時在通道中傳送和接收資料

另外通道中的資料不可以直接讀寫,而是必須和緩衝區進行互動,通道中的資料會寫到緩衝區,另外發送的資料也必須先到緩衝區才能通過通道傳送。

2.1.2、Buffer(緩衝區)

緩衝區本質上是一塊可以讀寫資料的記憶體,通道中的資料讀資料必須從緩衝區讀,寫資料也必須要寫到緩衝區。而想要讀寫緩衝區的資料必須呼叫緩衝區提供的API進行讀寫資料。

緩衝區的好處是讀寫兩端不需要關心彼此的狀態,而只需要和緩衝區互動即可。類似於MQ的訊息佇列,傳送方只需要把訊息傳送到佇列中即可,而消費者不需要關心傳送方的狀態,只需要不停從佇列中讀取資料即可。

而針對IO操作也是一樣,客戶端把資料傳送給服務端的緩衝區之後就結束了傳送資料的任務,而具體資料何時使用完全看服務端何時從緩衝區去讀資料。

2.1.3、Selector(多路複用選擇器)

Selector主要負責和channel互動,多個channel將自己感興趣的IO事件和自己繫結在一起註冊到Selector上,Selector可以同時監控多個Channel的狀態,如果發生了channel感興趣的事件,那麼就通知channel進行資料的讀寫操作。

如果沒有Selector,就需要每個channel連結成功就需要分配一個執行緒去負責channel資料的讀寫,而使用了Selector之後,只需要一個執行緒監控多個channel的狀態,只有有了真正的IO操作之後才會分配執行緒去處理真正的IO操作。

Selector的底層是通過作業系統的select、poll和epoll機制來實現的。

2.2、NIO的使用案例

NIO服務端案例程式碼如下:

 1 public class NioServer {
 2 
 3     /** 服務端通道 */
 4     private static ServerSocketChannel serverSocketChannel;
 5 
 6     /** 多路複用選擇器*/
 7     private static Selector selector;
 8 
 9     public static void main(String[] args) {
10         try {
11             //1.初始化伺服器
12             initServer();
13             //2.啟動伺服器
14             startServer();
15         }catch (Exception e){
16             e.printStackTrace();
17         }
18     }
19 
20     private static void initServer() throws IOException {
21         /** 1.建立服務端通道 */
22         serverSocketChannel = ServerSocketChannel.open();
23 
24         //設定通道為非阻塞型別
25         serverSocketChannel.configureBlocking(false);
26 
27         /** 2. 繫結監聽埠號 */
28         serverSocketChannel.socket().bind(new InetSocketAddress(8000));
29 
30         /** 3. 建立多路複用選擇器 */
31         selector = Selector.open();
32 
33         /** 4. 註冊通道監聽的事件 */
34         serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
35     }
36 
37     /**  啟動伺服器 */
38     private static void startServer() throws IOException {
39         System.out.println("Start Server...");
40         while (true){
41             /** 1.不停輪訓獲取所有的channel的狀態 */
42             selector.select(); //阻塞當前執行緒,直到至少有一個通道觸發了對應的事件
43             /** 2.獲取所有觸發了註冊事件的channel及事件集合 */
44             Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
45             /** 3.遍歷處理所有channel的事件 */
46             while(iterator.hasNext()){
47                 SelectionKey key = iterator.next();
48                 /** 4.根據不同的事件型別處理不同的業務邏輯 */
49                 if(key.isAcceptable()){
50                     //表示當前通道接收連線成功,主要用於服務端接收到客戶端連線成功
51                     SocketChannel channel = serverSocketChannel.accept();
52                     channel.configureBlocking(false);
53                     channel.register(selector, SelectionKey.OP_READ);
54                 }else if(key.isConnectable()){
55                     //表示當前通道連線成功,主要用於客戶端請求伺服器連線成功
56                 }else if(key.isReadable()){
57                     //表示當前通道有可讀的資料
58                     receiveMsg(key);
59                 }else if(key.isWritable()){
60                     //表示當前通道可以寫入資料,(網路不阻塞的情況下,基本上一直處於可寫狀態,除非緩衝區滿了)
61                 }
62                 iterator.remove();
63             }
64         }
65     }
66 
67     /** 讀取資料,當事件為可讀事件時呼叫 */
68     private static void receiveMsg(SelectionKey key) throws IOException {
69         SocketChannel channel = (SocketChannel) key.channel();
70         /** 1.分配2048大小的快取區大小 */
71         ByteBuffer buffer = ByteBuffer.allocate(2048);
72         /** 2.將channel中資料讀到緩衝區,並返回資料大小 */
73         int i = channel.read(buffer);
74         if(i != -1){
75             /** 3.從緩衝區獲取所有資料,解析成字串*/
76             String msg = new String(buffer.array()).trim();
77             System.out.println("伺服器接收到訊息:" + msg);
78             /** 4.呼叫write方法向channel中傳送資料 */
79             channel.write(ByteBuffer.wrap(("reply : " + msg).getBytes()));
80         }else {
81             channel.close();
82         }
83     }
84 }

NIO客戶端案例程式碼如下:

 1 public class NioClient {
 2 
 3     private SocketChannel channel;
 4 
 5     public static void main(String[] args) throws Exception {
 6         NioClient client = new NioClient();
 7         client.initClient();
 8         client.sendMsg("Hello NIO Server");
 9         client.receiveMsg();
10     }
11 
12     /** 初始化客戶端*/
13     private void initClient() throws Exception{
14         /** 1.建立客戶端通道,並繫結伺服器IP和埠號 */
15         channel = SocketChannel.open(new InetSocketAddress("localhost", 8000));
16     }
17 
18     /** 傳送資料到服務端*/
19     private void sendMsg(String msg) throws IOException {
20         byte[] bytes = msg.getBytes();
21         ByteBuffer buffer = ByteBuffer.wrap(bytes);
22         /** 向通道傳送資料 */
23         channel.write(buffer);
24         buffer.clear();
25     }
26 
27     /** 從伺服器接收資料*/
28     private void receiveMsg() throws IOException {
29         ByteBuffer buffer = ByteBuffer.allocate(2048);
30         channel.read(buffer);
31         System.out.println(new String(buffer.array()));
32         channel.close();
33     }
34 }

2.3、NIO的工作流程

服務端:

1、建立ServerSocketChannel通道物件,是所有客戶端通道的父通道,專門用於負責處理客戶端的連線請求

2、繫結伺服器監聽埠,設定是否阻塞模式

3、建立多路複用選擇器Selector物件,可以建立一個或者多個

4、將ServerSocketChannel以及監聽的客戶端連線事件(ACCEPT事件)一起註冊到Selector上(只需要監聽ACCEPT事件即可,專門用於處理客戶端的連線請求,至於和客戶端讀寫資料的互動再另外建立通道實現)

5、死迴圈不停的輪訓Selector上註冊的所有的通道是否觸發了註冊的事件

5.1、通過呼叫Selector的select()方法,該方法會阻塞當前執行緒,直到至少有一個註冊的通道觸發了對應的事件才會取消阻塞,然後通過SelectedKeys方法獲取所有觸發了事件的通道

5.2、遍歷所有的SelectionKey,根據觸發的事件的型別,進行不同的處理

6、當監聽到客戶端連線事件之後,為客戶端建立SocketChannel用於TCP資料通訊,並且將該通道和可讀事件(ON_READ)註冊到Selector上

7、當監聽到客戶端可讀事件之後,表示客戶端向伺服器傳送資料,那麼為該通道建立一定大小的緩衝區,將通道中的資料寫入到緩衝區

8、業務處理邏輯從緩衝區讀取客戶端傳送來的資料,進行解析和業務處理

9、伺服器通過呼叫channel的write方法回寫資料存入buffer中,(不需要關閉channel,channel是客戶端斷開了連線之後,服務端會接收到ON_READ事件,然後報錯就知道channel斷開了)

客戶端:

1、建立SocketChannel通道物件,並繫結伺服器IP和埠資訊進行連線請求

2、直接通過緩衝區向伺服器傳送資料

3、直接嘗試從通道中讀取資料發到緩衝區

三、NIO原始碼解析

3.1、伺服器的初始化

伺服器的初始化包括建立ServerSocketChannel通道的初始化和多路複用選擇器Selector的初始化

3.1.1、ServerSocketChannel初始化

ServerSocketChannel是通道介面Channel的實現類,主要用於伺服器客戶端連線的通訊,通過靜態方法open()方法建立,原始碼如下:

1 /** 建立 ServerSocketChannel物件 */
2     public static ServerSocketChannel open() throws IOException {
3         /** 通過SelectorProvider的openServerSocketChannel方法建立*/
4         return SelectorProvider.provider().openServerSocketChannel();
5     }

這裡是通過SelectorProvider的provider方法先獲取SelectorProvider物件,然後再呼叫SelectorProvidor的openServerSocketChannel方法建立,該方法程式碼如下:

1 public ServerSocketChannel openServerSocketChannel() throws IOException {
2         /** 直接建立ServerSocketChannel介面的實現類 ServerSocketChanelImpl物件 */
3         return new ServerSocketChannelImpl(this);
4     }

可以看出最終建立的ServerSocketChannel物件實際就是建立了一個ServerSocketChannelImpl物件

3.1.2、Selector初始化

Selector的初始化也是呼叫了Selector類的靜態方法open方法建立的,程式碼如下

1 public static Selector open() throws IOException {
2         /** 呼叫具體的SelectorProvider的openSelector方法 */
3         return SelectorProvider.provider().openSelector();
4     }

可以發現和ServerSocketChannel的open邏輯基本上一直,都是先獲取SelectorProvider物件,然後呼叫對應的openSelector方法來建立,只不過openSelector的實現這裡有多個子類都實現了該方法,因為Selector是完全依賴於底層作業系統的支援的,所以openSelector方法會根據當前作業系統的不同返回不同的Selector物件,Windows系統就會返回WindowsSelectorImpl物件,而Linux系統就可以根據具體使用哪一種多路複用機制來選用哪種Selector實現類,可以使用SelectorImpl、PollSelectorImpl、KQueueSelectorImpl(mac系統)等。不同電腦檢視openSelector方法的實現可能會不一樣,因為不同作業系統的JDK包不一樣,所以原始碼也不同,主要看當前作業系統支援哪一種Selector,所以openSelector的功能就是根據當前作業系統建立一個Selector物件

3.2、Selector的工作原理

Selector的工作流程主要步驟如下:

1、呼叫Selector的select()方法阻塞當前執行緒,直到有channel觸發了註冊的事件

2、呼叫Selector的selectedKeys()方法獲取所有通道和事件,事件和通道一起封裝成了SelectionKey物件

3、遍歷所有的SelectionKey集合,分別判斷事件的型別,執行對應的處理

SelectionKey類

SelectionKey類可以看作是channel和事件的封裝類,當一個channel觸發了對應的事件之後,就會將事件型別和Channel一起封裝成一個SelectionKey物件,而事件型別主要有以下四種:

int OP_READ = 1<<0 = 1 : 可讀事件,表示當前通道中有資料可以讀取(服務端和客戶端公用)

int OP_WRITE = 1<<2 = 4 :可寫事件,表示當前可以向通道中寫入資料(服務端和客戶端公用)

int OP_CONNECT= 1<<3 = 8 :連線事件,表示客戶端向服務端連線成功(使用者客戶端)

int OP_ACCEPT = 1<<4 = 16 :接收連線事件,表示服務端接收到客戶端連線成功(用於服務端)

SelectionKey針對不同事件提高了不同的方法,判斷是否觸發了對應的事件,方法如下:

 1  /** 是否觸發可讀事件 */
 2     public final boolean isReadable() {
 3         /** 獲取當前狀態 和 可讀事件值進行與運算 */
 4         return (readyOps() & OP_READ) != 0;
 5     }
 6 
 7     /** 是否觸發可寫事件 */
 8     public final boolean isWritable() {
 9         /** 獲取當前狀態 和 可寫事件進行與運算 */
10         return (readyOps() & OP_WRITE) != 0;
11     }
12 
13     /** 是否觸發連線成功事件 */
14     public final boolean isConnectable() {
15         /** 獲取當前狀態 和 連線成功事件進行與運算*/
16         return (readyOps() & OP_CONNECT) != 0;
17     }
18 
19     /** 是否接收連線成功事件 */
20     public final boolean isAcceptable() {
21         /** 獲取當前狀態 和 接收連線成功事件進行與運算*/
22         return (readyOps() & OP_ACCEPT) != 0;
23     }
24 
25     /** 獲取當前通道就緒的狀態值,由子類實現 */
26     public abstract int readyOps();

3.3、Selector的regist方法原始碼解析

Selector的regist方法用來註冊Channel和感興趣的事件,將channel和事件封裝成SelectionKey物件儲存在Selector中,原始碼如下:

 1 /**
 2      * 註冊通道和事件
 3      * @param var1 : 通道
 4      * @param var2 : 事件
 5      * */
 6     protected final SelectionKey register(AbstractSelectableChannel var1, int var2, Object var3) {
 7         if (!(var1 instanceof SelChImpl)) {
 8             throw new IllegalSelectorException();
 9         } else {
10             /** 構建SelectionKeyImpl物件 */
11             SelectionKeyImpl var4 = new SelectionKeyImpl((SelChImpl)var1, this);
12             var4.attach(var3);
13             synchronized(this.publicKeys) {
14                 /** 註冊SelectionKey */
15                 this.implRegister(var4);
16             }
17             /** 設定SelectionKey物件感興趣的事件 */
18             var4.interestOps(var2);
19             return var4;
20         }
21     }

3.4、Selector的select方法原始碼解析

 1 public int select() throws IOException {
 2         //呼叫內部過載方法select方法
 3         return this.select(0L);
 4     }
 5 
 6     public int select(long var1) throws IOException {
 7         if (var1 < 0L) {
 8             throw new IllegalArgumentException("Negative timeout");
 9         } else {
10             //呼叫內部的lockAndDoSelect方法
11             return this.lockAndDoSelect(var1 == 0L ? -1L : var1);
12         }
13     }
14 
15     private Set<SelectionKey> publicKeys;
16     private Set<SelectionKey> publicSelectedKeys;
17 
18     private int lockAndDoSelect(long var1) throws IOException {
19         /** 將當前Selector物件鎖住*/
20         synchronized(this) {
21             //1.判斷當前Selector是否是開啟狀態
22             if (!this.isOpen()) {
23                 throw new ClosedSelectorException();
24             } else {
25                 int var10000;
26                 /**
27                  * 鎖住publicKeys物件
28                  * */
29                 synchronized(this.publicKeys) {
30                     /**
31                      * 所以publicSelectedKeys物件
32                      * */
33                     synchronized(this.publicSelectedKeys) {
34                         /** 呼叫內部 doSelect方法
35                          *  doSelect方法的具體是否由子類實現,根據不同的Selector型別呼叫本地方法,
36                          *  而本地方法的實現就是呼叫作業系統的多路複用技術select、poll或epoll機制返回結果
37                          * */
38                         var10000 = this.doSelect(var1);
39                     }
40                 }
41                 return var10000;
42             }
43         }
44     }
45 
46     /** SelectionKey陣列 */
47     protected SelectionKeyImpl[] channelArray;
48 
49     /** 以PollSelectorImpl實現類為例*/
50     protected int doSelect(long var1) throws IOException {
51         if (this.channelArray == null) {
52             throw new ClosedSelectorException();
53         } else {
54             //清理已經無效的SelectionKey
55             this.processDeregisterQueue();
56 
57             try {
58                 /** 呼叫begin方法使得執行緒進入阻塞狀態,直到有SelectionKey觸發了事件*/
59                 this.begin();
60                 /** 呼叫本地方法poll方法,本質是呼叫作業系統的poll方法*/
61                 this.pollWrapper.poll(this.totalChannels, 0, var1);
62             } finally {
63                 this.end();
64             }
65 
66             this.processDeregisterQueue();
67             /** 統計觸發了事件的SelectionKey個數,並新增到Set<SelectionKey>集合中*/
68             int var3 = this.updateSelectedKeys();
69             if (this.pollWrapper.getReventOps(0) != 0) {
70                 this.pollWrapper.putReventOps(0, 0);
71                 synchronized(this.interruptLock) {
72                     IOUtil.drain(this.fd0);
73                     this.interruptTriggered = false;
74                 }
75             }
76 
77             return var3;
78         }
79     }
有三個大小為1025大小的陣列,1位存放發生事件的socket的總數,後面存放發生事件的socket控制代碼個數,分別是readFds、writeFds和exceptFds,分別對應讀事件、寫事件、異常事件,然後呼叫本地的poll方法,如果有事件發生統計數量封裝成SelectKey返回,如果沒有資料就一直阻塞知道有資料返回或者是達到超時時間返回。

Tips:

1、遍歷SelectionKey集合之後,需要將SelectionKey從集合中刪除,否則下一次呼叫select方法時還會返回

2、呼叫select()方法之後主執行緒會一直被阻塞,直到有channel觸發了事件,通過呼叫wakeup方法喚醒主執行緒

3.3、Buffer的原始碼解析

Buffer是NIO中IO資料儲存的緩衝區,資料從channel中寫入到Buffer中或者資料由channel從Buffer中讀取資料。而緩衝區的本質就是一個數組,如果是ByteBuffer那麼就是byte陣列,如果是CharBuffer,那麼就是char陣列。

3.3.1、Buffer的核心屬性

而Buffer的使用又離不開陣列位置的標記,用來標記Buffer陣列的核心變數分別如下:

capacity:陣列容量,陣列的總大小,初始化Buffer時設定,固定不變

position:當前的位置,初始化值為0,每向陣列中寫1位資料,position的值就向後移動一位,所以position的取值範圍為 0 ~ capacity-1;當Buffer從寫模式切換到讀模式之後,position值置位0,每讀1位資料,position向後移動一位。

limit:表示當前可讀或可寫資料的上限,寫模式下預設為capacity;讀模式下值為position的值

mark:標記位置,呼叫mark方法之後將mark值設定為position值用來標記,當呼叫reset方法之後position值再恢復到mark值,預設為-1

四個屬性之間的大小關係為 : 0 <= mark <= position <= limit <= capacity

3.3.2、Buffer的核心方法

put方法向陣列中插入資料,position值隨著插入資料而變化,假設插入5個數據,那麼position值為5,其他變數值不變

flip方法將緩衝區資料由寫模式切換成讀模式,此時limit值設定為position,position重新置為0,其他變數值不變

rewind方法當讀取資料讀到一半時發現數據有問題,需要從頭開始讀起,此時可以呼叫rewind方法,該方法和flip方法類似,但是不影響limit屬性,而只是將position置為0,mark設定為-1,相當於讀模式下從頭開始讀資料;寫模式下從頭開始寫資料

compact方法當讀取資料讀到一半時,又需要重新切換到寫模式時,此時已讀部分資料的空間就無法再寫入資料,那麼就會造成空間的浪費,此時可以呼叫compact方法將剩餘的空間進行壓縮,實現邏輯就是將當前剩餘未讀的資料複製到陣列的0的位置,而position設定為下一個可以寫的位置,limit重新設定為最大值capacity。

mark方法:用來標記當前的position位置,將mark設定為position值

reset方法:將position重新設定上一次標記的值,也就是position=mark值

clear方法:清空緩衝區,設定position=0,mark=-1,limit=capacity,這裡的clear只是設定各個位置屬性的值,而陣列內的資料並不會真的被清空

3.3.3、圖解Buffer的核心方法邏輯

1、初始化容量為10的陣列,如下圖:

初始化時容量為10,此時mark為預設值-1,position為預設值0,limit和capacity都是容量的值為10

2、呼叫put方法向陣列中寫入5個數據,此時position值為5,limit、capacity和mark值不變,如下圖:

3、此時不需要再寫資料,而是需要從緩衝區讀資料時,呼叫flip方法將寫模式切換成讀模式,此時position值為0,limit值為當前position值為5,capacity和mark值不變,如下圖:

4、當讀取資料讀到第2個數據時,為了防止後面的資料讀取失敗,可以標記當前的位置,呼叫mark方法將mark設為position的值,而其他變數不變,如下圖:

5、當讀資料到位置2之後,發現又需要切換到寫模式,那麼此時就需要重新向資料中寫入資料,此時0-4的位置已經有了資料,就需要從5的位置開始寫入,而已經讀取的0-2兩個位置已經被浪費了,所以為了避免空間的浪費,可以呼叫compact方法進行空間壓縮,

壓縮的邏輯為將剩餘所有未讀的資料複製到陣列下標為0的位置,然後將position設定為下一個可以寫的位置i,limit設定為capacity的值。如下圖示:

將資料“CDE”移動陣列為0的位置,此時position值為3,雖然3-4位置已經有了資料"DE",但是馬上就會被新寫入的資料覆蓋掉,所以不會有重複資料的問題。