Java NIO總結

一、NIO和I/O區別

I/O和NIO的區別在於數據的打包和傳輸方式。html

  • I/O流的方式處理數據java

  • NIO塊的方式處理數據數組

面向流 的 I/O 系統一次一個字節地處理數據。一個輸入流產生一個字節的數據,一個輸出流消費一個字節的數據。爲流式數據建立過濾器很是容易。連接幾個過濾器,以便每一個過濾器只負責單個複雜處理機制的一部分,這樣也是相對簡單的。面向流的 I/O 一般至關慢。服務器

一個 面向塊 的 I/O 系統以塊的形式處理數據。每個操做都在一步中產生或者消費一個數據塊。按塊處理數據比按(流式的)字節處理數據要快得多。可是面向塊的I/O比較複雜。NIO的主要應用在高性能、高容量服務端應用程序。網絡

IO NIO
面向流 面向塊,緩衝
阻塞IO 非阻塞IO
Selector

NIO的核心梳理

一、Channels and Buffers(通道和緩衝區)
標準的IO基於字節流和字符流進行操做的,而NIO是基於通道(Channel)和緩衝區(Buffer)進行操做,數據老是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。dom

二、Non-blocking IO(非阻塞IO)
Java NIO能夠非阻塞的方式使用IO,例如:當線程從通道讀取數據到緩衝區時,線程仍是能夠進行其餘事情。當數據被寫入到緩衝區時,線程能夠繼續處理它。從緩衝區寫入通道也相似。異步

三、Selectors(選擇器)
Java NIO引入了選擇器的概念,選擇器用於監聽多個通道的事件(好比:鏈接打開,數據到達)。所以,單個的線程能夠監聽多個數據通道。socket

二、Buffers(緩衝區)

通道和緩衝區是 NIO 中的核心對象,幾乎在每個 I/O 操做中都要使用它們。性能

一個 Buffer 實質上是一個容器對象,它包含一些要寫入或者剛讀出的數據。spa

在 NIO 庫中,全部數據都是用緩衝區處理的。在讀取數據時,它是直接讀到緩衝區中的。在寫入數據時,它是寫入到緩衝區中的。任什麼時候候訪問 NIO 中的數據,您都是將它放到緩衝區中。

緩衝區實質上就是一個數組,一般它是一個字節數組,可是也可使用其餘種類的數組。但它不只僅是一個數組,緩衝區還提供了對數據的結構化訪問,並且還能夠跟蹤系統的讀/寫進程。

Buffer 類型

最經常使用的緩衝區類型是 ByteBuffer。一個 ByteBuffer 能夠在其底層字節數組上進行 get/set 操做(即字節的獲取和設置)。

ByteBuffer 不是 NIO 中惟一的緩衝區類型。事實上,對於每一種基本 Java 類型都有一種緩衝區類型:
image_1bc2m32k0rp7147du6n95f1fjg9.png-25.5kB

Buffer抽象類中提供了須要處理的方法類型。

Buffer 用法

使用Buffer讀寫數據通常遵循如下四個步驟:

  1. 寫入數據到Buffer

  2. 調用flip()方法

  3. 從Buffer中讀取數據

  4. 調用clear()方法或者compact()方法

當向buffer寫入數據時,buffer會記錄下寫了多少數據。一旦要讀取數據,須要經過flip()方法將Buffer從寫模式切換到讀模式。在讀模式下,能夠讀取以前寫入到buffer的全部數據。一旦讀完了全部的數據,就須要清空緩衝區,讓它能夠再次被寫入。

有兩種方式能清空緩衝區:調用clear()或compact()方法。

clear()方法會清空整個緩衝區。compact()方法只會清除已經讀過的數據。任何未讀的數據都被移到緩衝區的起始處,新寫入的數據將放到緩衝區未讀數據的後面。

try (RandomAccessFile aFile = new RandomAccessFile("F:\\1.txt", "rw")) {
            //從RandomAccesFile獲取通道
            FileChannel inChannel = aFile.getChannel();
            
            //建立一個緩衝區並在其中放入一些數據
            ByteBuffer buf = ByteBuffer.allocate(48);
            int bytesRead = inChannel.read(buf); //read into buffer.
            
            //檢查狀態
            while (bytesRead != -1) {
                //flip() 方法讓緩衝區能夠將新讀入的數據寫入另外一個通道。
                buf.flip();  //make buffer ready for read
                
                while (buf.hasRemaining()) {
                    System.out.print((char) buf.get()); // read 1 byte at a time
                }
                
                buf.clear(); //make buffer ready for writing
                bytesRead = inChannel.read(buf);
            }
        }

capacity & position & limit

緩衝區本質上是一塊能夠寫入數據,而後能夠從中讀取數據的內存。這塊內存被包裝成NIO Buffer對象,並提供了一組方法,用來方便的訪問該塊內存。

緩衝區對象有三個基本屬性:

  • 容量Capacity:緩衝區能容納的數據元素的最大數量,在緩衝區建立時設定,沒法更改

  • 上界Limit:代表還有多少數據須要取出(在從緩衝區寫入通道時),或者還有多少空間能夠放入數據(在從通道讀入緩衝區時),limit不能大於capacity

  • 位置Position:下一個要被讀或寫的元素的索引

這三個變量一塊兒能夠跟蹤緩衝區的狀態和它所包含的數據。

四個屬性老是遵循這樣的關係:0<=mark<=position<=limit<=capacity。下圖是新建立的容量爲10的緩衝區邏輯視圖:
15184237_ffbM

寫入模式
buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o');
五次調用put後的緩衝區:
15184241_NUYi

此時,limit表示還有多少空間能夠放入數據。position最大可爲capacity-1。

讀取模式
如今緩衝區滿了,咱們必須將其清空。咱們想把這個緩衝區傳遞給一個通道,以使內容能被所有寫出,但如今執行get()無疑會取出未定義的數據。咱們必須將posistion設爲0,而後通道就會從正確的位置開始讀了,但讀到哪算讀完了呢?這正是limit引入的緣由,它指明緩衝區有效內容的未端。這個操做 在緩衝區中叫作翻轉:buffer.flip()。

flip這個方法作兩件很是重要的事:

  1. 將 limit 設置爲當前 position。

  2. 將 position 設置爲 0。

15184249_V47C

rewind操做與flip類似,但不影響limit。

clear
最後一步是調用緩衝區的 clear() 方法。這個方法重設緩衝區以便接收更多的字節。
Clear 作兩種很是重要的事情:

1.將 limit 設置爲與 capacity 相同。
2.設置 position 爲 0。
15184237_ffbM

clear()與compact()區別
一、clear()方法,position將被設回0,limit被設置成 capacity的值。Buffer中有一些未讀的數據,調用clear()方法,數據將「被遺忘」
二、compact()方法將全部未讀的數據拷貝到Buffer起始處。而後將position設到最後一個未讀元素正後面。limit屬性依然像clear()方法同樣,設置成capacity。

Buffer的分配

在可以讀和寫以前,必須有一個緩衝區。要建立緩衝區,必需要進行分配,。咱們使用靜態方法 allocate() 來分配緩衝區,每個Buffer類都有一個allocate方法。

//分配48字節capacity的ByteBuffer的例子。
ByteBuffer buf = ByteBuffer.allocate(48);

//分配一個可存儲1024個字符的CharBuffer:
CharBuffer buf = CharBuffer.allocate(1024);

Buffer寫入

寫數據到Buffer有兩種方式:

  1. 從Channel寫到Buffer。

  2. 經過Buffer的put()方法寫到Buffer裏。

//從Channel寫到Buffer
int bytesRead = inChannel.read(buf); //read into buffer.

//經過put方法寫Buffer的例子:
buf.put(127);

Buffer讀取

從Buffer中讀取數據有兩種方式:

  1. 從Buffer讀取數據到Channel。

  2. 使用get()方法從Buffer中讀取數據。

//read from buffer into channel.
int bytesWritten = inChannel.write(buf);

//使用get()方法從Buffer中讀取數據
byte aByte = buf.get();

get方法有不少版本,容許你以不一樣的方式從Buffer中讀取數據。例如,從指定position讀取,或者從Buffer中讀取數據到字節數組。

rewind()方法

Buffer.rewind()將position設回0,因此你能夠重讀Buffer中的全部數據。limit保持不變,仍然表示能從Buffer中讀取多少個元素(byte、char等)。

mark()與reset()

經過調用Buffer.mark()方法,能夠標記Buffer中的一個特定position。以後能夠經過調用Buffer.reset()方法恢復到這個position。

Buffer比較

equals()與compareTo()方法

可使用equals()和compareTo()方法兩個Buffer。

equals()

當知足下列條件時,表示兩個Buffer相等:

  • 有相同的類型(byte、char、int等)。

  • Buffer中剩餘的byte、char等的個數相等。

  • Buffer中全部剩餘的byte、char等都相同。

equals只是比較Buffer的一部分,不是每個在它裏面的元素都比較。實際上,它只比較Buffer中的剩餘元素。

compareTo()

compareTo()方法比較兩個Buffer的剩餘元素(byte、char等), 若是知足下列條件,則認爲一個Buffer「小於」另外一個Buffer:

  • 第一個不相等的元素小於另外一個Buffer中對應的元素

  • 第一個Buffer的元素個數比另外一個少

三、Channel

Java NIO的通道相似流,但又有些不一樣:

  1. 既能夠從通道中讀取數據,又能夠寫數據到通道。但流的讀寫一般是單向的。

  2. 通道能夠異步地讀寫。

  3. 通道中的數據老是要先讀到一個Buffer,或者老是要從一個Buffer中寫入。

Channel類型

image_1bca9kutq1dno13o3oeqk52brg13.png-64.7kB

  • FileChannel 從文件中讀寫數據。

  • DatagramChannel 能經過UDP讀寫網絡中的數據。

  • SocketChannel 能經過TCP讀寫網絡中的數據。

  • ServerSocketChannel能夠監聽新進來的TCP鏈接,像Web服務器那樣。對每個新進來的鏈接都會建立一個SocketChannel。

close Channel

與緩衝區不一樣,通道不能被重複使用;關閉通道後,通道將再也不鏈接任何東西,任何的讀或寫操做都會致使ClosedChannelException。

調用通道的close()方法時,可能會致使線程暫時阻塞,就算通道處於非阻塞模式也不例外。若是通道實現了InterruptibleChannel接 口,那麼阻塞在該通道上的一個線程被中斷時,該通道將被關閉,被阻塞線程也會拋出ClosedByInterruptException異常。

當一個通道 關閉時,休眠在該通道上的全部線程都將被喚醒並收到一個AsynchronousCloseException異常。

Scatter/Gather

scatter/gather用於描述從Channel中讀取或者寫入到Channel的操做。

  • 分散(scatter)從Channel中讀取是指在讀操做時將讀取的數據寫入多個buffer中。所以,Channel將從Channel中讀取的數據"分散(scatter)"到多個Buffer中。

  • 彙集(gather)寫入Channel是指在寫操做時將多個buffer的數據寫入同一個Channel,所以,Channel 將多個Buffer中的數據"彙集(gather)"後發送到Channel。

scatter / gather常常用於須要將傳輸的數據分開處理的場合,例如傳輸一個由消息頭和消息體組成的消息,你可能會將消息體和消息頭分散到不一樣的buffer中,這樣你能夠方便的處理消息頭和消息體。

Scatter Reader

image_1bca92dvmnck1g971sn61vkon0f9.png-10.4kB

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

ByteBuffer[] bufferArray = { header, body };

channel.read(bufferArray);

buffer首先被插入到數組,而後再將數組做爲channel.read() 的輸入參數。read()方法按照buffer在數組中的順序將從channel中讀取的數據寫入到buffer,當一個buffer被寫滿後,channel緊接着向另外一個buffer中寫。

Scattering Reads在移動下一個buffer前,必須填滿當前的buffer,這也意味着它不適用於動態消息(譯者注:消息大小不固定)。換句話說,若是存在消息頭和消息體,消息頭必須完成填充(例如 128byte),Scattering Reads才能正常工做。

Gathering Writes

Gathering Writes是指數據從多個buffer寫入到同一個channel。以下圖描述:
image_1bca94vq9kmk1n11tfmug61dmjm.png-9.9kB

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

//write data into buffers

ByteBuffer[] bufferArray = { header, body };

channel.write(bufferArray);

buffers數組是write()方法的入參,write()方法會按照buffer在數組中的順序,將數據寫入到channel,注意只有position和limit之間的數據纔會被寫入。所以,若是一個buffer的容量爲128byte,可是僅僅包含58byte的數據,那麼這58byte的數據將被寫入到channel中。所以與Scattering Reads相反,Gathering Writes能較好的處理動態消息。

FileChannel

Java NIO中的FileChannel是一個鏈接到文件的通道。能夠經過文件通道讀寫文件。FileChannel沒法設置爲非阻塞模式,它老是運行在阻塞模式下。

FileChannel讀數據

由於沒法保證write()方法一次能向FileChannel寫入多少字節,所以須要重複調用write()方法,直到Buffer中已經沒有還沒有寫入通道的字節。

//經過inputstream或者RandomAccessFile,打開FileChannel
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

//往buffer裏面讀取數據
ByteBuffer buf = ByteBuffer.allocate(48);

//int值表示了有多少字節被讀到了Buffer中。若是返回-1,表示到了文件末尾。
int bytesRead = inChannel.read(buf);

FileChannel寫數據

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {
    channel.write(buf);
}

//用完FileChannel後必須將其關閉
channel.close();

四、Selector

Selector(選擇器)是Java NIO中可以檢測一到多個NIO通道,並可以知曉通道是否爲諸如讀寫事件作好準備的組件。這樣,一個單獨的線程能夠管理多個channel,從而管理多個網絡鏈接。

對於操做系統來講,線程之間上下文切換的開銷很大,並且每一個線程都要佔用系統的一些資源(如內存)。所以,使用的線程越少越好。

Selector的建立與註冊

使用Selector.open()方法建立Selector,爲了將Channel和Selector配合使用,必須將channel註冊到selector上。經過SelectableChannel.register()方法來實現。以下所示

//建立Selector
Selector selector = Selector.open();

//向Selector註冊通道
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

與Selector一塊兒使用時,Channel必須處於非阻塞模式下。這意味着不能將FileChannel與Selector一塊兒使用,由於FileChannel不能切換到非阻塞模式。而套接字通道均可以。

register()方法的第二個參數。這是一個「interest集合」,意思是在經過Selector監聽Channel時對什麼事件感興趣。能夠監聽四種不一樣類型的事件:

事件類型 常量
Connect SelectionKey.OP_CONNECT
Accept SelectionKey.OP_ACCEPT
Read SelectionKey.OP_READ
Write SelectionKey.OP_WRITE

通道觸發了一個事件意思是該事件已經就緒。因此,某個channel成功鏈接到另外一個服務器稱爲「鏈接就緒」。一個server socket channel準備好接收新進入的鏈接稱爲「接收就緒」。一個有數據可讀的通道能夠說是「讀就緒」。等待寫數據的通道能夠說是「寫就緒」。

SelectionKey

當向Selector註冊Channel時,register()方法會返回一個SelectionKey對象。這個對象包含了一些屬性:

  1. interest集合

  2. ready集合

  3. Channel

  4. Selector

  5. 附加的對象

interest集合

interest集合是你所選擇的感興趣的事件集合。能夠經過SelectionKey讀寫interest集合,像這樣:

int interestSet = selectionKey.interestOps();

boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;

|位與操做interest 集合和給定的SelectionKey常量,能夠肯定某個肯定的事件是否在interest 集合中。

ready集合

ready 集合是通道已經準備就緒的操做的集合。在一次選擇(Selection)以後,會首先訪問這個ready set。能夠這樣訪問ready集合:

int readySet = selectionKey.readyOps();

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

能夠用像檢測interest集合那樣的方法,來檢測channel中什麼事件或操做已經就緒。可是,也可使用以上四個方法,它們都會返回一個布爾類型。

Channel + Selector

從SelectionKey訪問Channel和Selector很簡單。以下:

Channel  channel  = selectionKey.channel();
Selector selector = selectionKey.selector();

附加的對象
能夠將一個對象或者更多信息附着到SelectionKey上,能夠方便識別某個給定的通道。例如,能夠附加 與通道一塊兒使用的Buffer,或是包含彙集數據的某個對象。使用方法以下:

selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();

//用register()方法向Selector註冊Channel的時候附加對象
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

Selector選擇通道

一旦向Selector註冊了一或多個通道,就能夠調用select()方法,選擇已經準備就緒的事件的通道類型。
select方法

int select() //阻塞到至少有一個通道在你註冊的事件上就緒了
int select(long timeout)//和select()同樣,除了最長會阻塞timeout毫秒(參數)。
int selectNow()//不會阻塞,無論什麼通道就緒都馬上返回

select()方法返回的int值表示有多少通道已經就緒,而後能夠經過調用selector的selectedKeys()方法,訪問「已選擇鍵集(selected key set)」中的就緒通道。以下所示:

//訪問「已選擇鍵集(selected key set)」中的就緒通道
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
    } else if (key.isConnectable()) {
        // a connection was established with a remote server.
    } else if (key.isReadable()) {
        // a channel is ready for reading
    } else if (key.isWritable()) {
        // a channel is ready for writing
    }
    keyIterator.remove();
}

這個循環遍歷已選擇鍵集中的每一個鍵,並檢測各個鍵所對應的通道的就緒事件。

注意每次迭代末尾的keyIterator.remove()調用。Selector不會本身從已選擇鍵集中移除SelectionKey實例。必須在處理完通道時本身移除。下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中。

wakeUp()

某個線程調用select()方法後阻塞了,即便沒有通道已經就緒,也有辦法讓其從select()方法返回。只要讓其它線程在第一個線程調用select()方法的那個對象上調用Selector.wakeup()方法便可。阻塞在select()方法上的線程會立馬返回。

若是有其它線程調用了wakeup()方法,但當前沒有線程阻塞在select()方法上,下個調用select()方法的線程會當即「醒來(wake up)」。

close()

用完Selector後調用其close()方法會關閉該Selector,且使註冊到該Selector上的全部SelectionKey實例無效。通道自己並不會關閉。

五、 管道(pip)

管道是2個線程之間的單向數據鏈接。Pipe有一個source通道和一個sink通道。數據會被寫到sink通道,從source通道讀取。

原理圖以下:
image_1bd1glsp81i341t187to84o7j39.png-10.9kB

管道寫入數據

//Pipe.open()方法打開管道
Pipe pipe = Pipe.open();

//訪問sink通道,向管道寫數據
Pipe.SinkChannel sinkChannel = pipe.sink();

//調用SinkChannel的write()方法,將數據寫入SinkChannel
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();

while(buf.hasRemaining()) {
    sinkChannel.write(buf);
}

管道讀取數據

//訪問source通道
Pipe.SourceChannel sourceChannel = pipe.source();

//調用source通道的read()方法來讀取數據
ByteBuffer buf = ByteBuffer.allocate(48);

//read()方法返回的int值表示多少字節被讀進了緩衝區
int bytesRead = sourceChannel.read(buf);

參考資料引用

一、NIO 入門
二、Java NIO教程
三、理解Java NIO

相關文章
相關標籤/搜索