Java NIO

目錄

1   NIO概述

  1.1  Channel和Buffer

  1.2   Selector

2   NIO核心API

  2.1   Channel   通道

  2.2   Buffer   緩衝區(基本用法,capacity,position,limit,構造方法,讀寫數據,rewind(),clear(),compact(),mark(),reset(),Buffer對象比較)

  2.3   Buffer的 Scatter/Gather

  2.4   Channel間的數據傳輸(transferFrom()   transferTo())

  2.5   Selector(建立,註冊Channel,SelectionKey,Selector選擇Channel,WakeUp(),close())

3   IO與NIO區別

  3.1   面向流VS面向緩衝區

  3.2   阻塞VS非阻塞

  3.3   底層實現的區別

 

NIO概述

NIO即New IO/Non-Blocking IO,JDK1.4中引入。NIO和IO有相同的做用和目的,但實現方式不一樣,NIO主要用到的是塊,因此NIO的效率要比IO高不少html

Java API中提供了兩套NIO,一套是針對標準輸入輸出NIO,另外一套就是網絡編程NIOjava

Java NIO 由如下幾個核心部分組成編程

  • Channels
  • Buffers
  • Selectors

雖然Java NIO 中除此以外還有不少類和組件,但Channel,Buffer 和 Selector 構成了核心的API其它組件,如Pipe和FileLock,只不過是與三個核心組件共同使用的工具類數組

Channel和Buffer

基本上,全部的 IO 在NIO 中都從一個Channel 開始Channel 有點像流 數據能夠從Channel讀到Buffer中,也能夠從Buffer 寫到Channel緩存

Channel和Buffer有好幾種類型。下面是JAVA NIO中的一些主要Channel的實現,這些Channel涵蓋了UDP 和 TCP 網絡IO,以及文件IO服務器

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

這些類一塊兒的有一些有趣的接口,但爲簡單起見,我儘可能在概述中不提到它們網絡

如下是Java NIO裏關鍵的Buffer實現,這些Buffer覆蓋了你能經過IO發送的基本數據類型byte, short, int, long, float, double 和 charapp

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

Java NIO 還有個 MappedByteBuffer,用於表示內存映射文件dom

Selector

Selector容許單線程處理多個 Channel。若是你的應用打開了多個鏈接(通道),但每一個鏈接的流量都很低,使用Selector就會很方便。例如,在一個聊天服務器中異步

這是在一個單線程中使用一個Selector處理3個Channel的圖示

使用Selector,得向Selector註冊Channel,而後調用它的select()方法。這個方法會一直阻塞到某個註冊的通道有事件就緒。一旦這個方法返回線程就能夠處理這些事件,事件的例子有如新鏈接進來,數據接收等

事件名 對應值
服務端接收客戶端鏈接事件 SelectionKey.OP_ACCEPT(16)
客戶端鏈接服務端事件 SelectionKey.OP_CONNECT(8)
讀事件 SelectionKey.OP_READ(1)
寫事件 SelectionKey.OP_WRITE(4)

NIO核心API

Channel   通道

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

  • 通道是雙向的,既能夠從通道中讀取數據,又能夠寫數據到通道。但流的讀寫一般是單向
  • 通道能夠異步地讀寫
  • 通道中的數據老是先讀到一個Buffer老是從一個Buffer中寫入

 

FileChannel 從文件讀寫數據

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

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

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

 

Buffer   緩衝區

Buffer用於和Channel進行交互。如你所知,數據是從Channel讀入Buffer,從Buffer寫入到Channel中的

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

基本用法

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

  1. 入數據到Buffer
  2. 調用flip()方法
  3. 從Buffer中讀取數據
  4. 調用clear()方法或者compact()方法

向buffer寫入數據buffer會記錄寫了多少數據。一旦要讀取數據,須要經過flip()方法將Buffer從寫模式切換到讀模式。在讀模式下,能夠讀取以前寫入到buffer的全部數據

一旦讀完了全部的數據,就須要清空緩衝區讓它可再次被寫入。有兩種方式清空緩衝區:調用clear()或compact()方法。clear()方法會清空整個緩衝區compact()方法只會清除已經讀過的數據,任何未讀的數據都被移到緩衝區的起始處新寫入的數據放到緩衝區未讀數據的後面

Buffer的capacity,position和limit

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

爲了理解Buffer的工做原理,須要熟悉它的三個屬性

  • capacity
  • position
  • limit

position和limit含義取決於Buffer處在讀模式仍是寫模式無論Buffer處在什麼模式capacity含義老是同樣

關於capacity,position和limit在讀寫模式中的說明

capacity

做爲一個內存塊,Buffer有一個固定的大小值,也叫「capacity」。你只能往裏寫capacity個byte、long,char等類型。一旦Buffer滿了,須要將其清空(經過讀數據或者清除數據才能繼續往裏寫數據

position

當你寫數據到Buffer中時,position表示當前位置初始position值爲0。當一個byte、long等數據寫到Buffer後position向後移動到下一個可插入數據的Buffer單元。position最大可爲capacity-1

數據時,也是從某個特定位置讀。當將Buffer從寫模式切換到讀模式position會被重置爲0. 當從Buffer的position處讀數據時position向後移動到下一個可讀的位置

limit

寫模式下,Buffer的limit表示最多能往Buffer裏寫多少數據寫模式下limit等於Buffer的capacity

切換Buffer到讀模式時, limit表示最多能讀到多少數據。所以,當切換Buffer到讀模式時,limit被設置成寫模式下的position值。換句話說,你能讀到以前寫入的全部數據(limit被設置成已寫數據的數量,這個值在寫模式下就是position

Buffer構造方法

想得到一個Buffer對象首先要進行分配。 每個Buffer類都有一個allocate方法。下面是一個分配48字節capacity的ByteBuffer的例子。

1 ByteBuffer buf = ByteBuffer.allocate(48);

這是分配一個可存儲1024字符的CharBuffer:

1 CharBuffer buf = CharBuffer.allocate(1024);

向Buffer中寫數據

寫數據到Buffer有種方式:

  • Channel寫到Buffer
  • 經過Buffer的put()方法寫到Buffer裏

從Channel寫到Buffer的例子

1 int bytesRead = inChannel.read(buf); //read into buffer.

經過put方法寫Buffer的例子

1 buf.put(127);

put方法有不少版本,容許你以不一樣的方式把數據寫入到Buffer中。例如, 寫到一個指定的位置,或者把一個字節數組寫入到Buffer。 更多Buffer實現的細節參考JavaDoc

flip()方法

flip方法將Buffer從寫模式切換到讀模式。調用flip()方法會將position設回0,並將limit設置成以前position的值(寫模式下,當前buffer已經存在數據的長度)

換句話說,position如今用於標記讀的位置,limit表示以前寫進了多少個byte、char等 —— 如今能讀取多少個byte、char等

從Buffer中讀取數據

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

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

從Buffer讀取數據到Channel的例子

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

使用get()方法從Buffer中讀取數據的例子

1 byte aByte = buf.get();

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

rewind()方法

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

clear()與compact()方法

一旦讀完Buffer中的數據,須要讓Buffer準備好再次被寫入。能夠經過clear()或compact()方法來完成

若是調用的是clear()方法,position將被設回0limit設置成 capacity的值。換句話說,Buffer 被清空了。Buffer中的數據並未清除,只是這些標記告訴咱們能夠從哪裏開始往Buffer裏寫數據

若是Buffer中有一些未讀的數據,調用clear()方法,數據將「被遺忘」,意味着再也不有任何標記會告訴你哪些數據被讀過,哪些尚未

若是Buffer中仍有未讀的數據,且後續還須要這些數據,可是此時想要先寫些數據,那麼使用compact()方法

compact()方法將全部未讀的數據拷貝到Buffer起始處。而後將position設到最後一個未讀元素後面limit屬性依然像clear()方法同樣設置成capacity。如今Buffer準備好寫數據了,可是不會覆蓋未讀的數據

mark()與reset()方法

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

1 buffer.mark();
2 //call buffer.get() a couple of times, e.g. during parsing.
3 buffer.reset();  //set position back to mark.

equals()與compareTo()方法

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

equals()

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

  1. 相同類型(byte、char、int等)。
  2. Buffer中剩餘的byte、char等的個數相等
  3. Buffer中全部剩餘的byte、char等值都相同

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

compareTo()

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

  1. 第一個不相等的元素小於另外一個Buffer中對應的元素 
  2. 全部元素都相等,但第一個Buffer比另外一個先耗盡(第一個Buffer的元素個數比另外一個)    

剩餘元素是從 position到limit之間的元素

 

 

Buffer的 Scatter/Gather

Java NIO支持scatter/gather,scatter/gather用於描述從Channel中讀取或者寫入到Channel的操做
分散(scatter)從Channel中讀取是指在讀操做時將讀取的數據寫入到多個buffer
彙集(gather)寫入Channel是指在寫操做時將多個buffer的數據寫入同一個Channel

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

Scattering Reads

Scattering Reads是指數據從一個channel讀取到多個buffer中。以下圖描述

Java NIO: Scattering Read
1 ByteBuffer header = ByteBuffer.allocate(128);
2 ByteBuffer body   = ByteBuffer.allocate(1024);
3 ByteBuffer[] bufferArray = { header, body };
4 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。以下圖描述

Java NIO: Gathering Write

 

 
1 ByteBuffer header = ByteBuffer.allocate(128);
2 ByteBuffer body   = ByteBuffer.allocate(1024);
3 //write data into buffers
4 ByteBuffer[] bufferArray = { header, body };
5 channel.write(bufferArray);

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

 

Channel間的數據傳輸

Java NIO中,若是兩個通道有一個是FileChannel,那你能夠直接將數據從一個channel傳輸到另一個channel

transferFrom()       其它Channel到FileChannel

FileChannel的transferFrom()方法能夠將數據從源通道傳輸到FileChannel中

01 RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt""rw");
02 FileChannel      fromChannel = fromFile.getChannel();
03 RandomAccessFile toFile = new RandomAccessFile("toFile.txt""rw");
04 FileChannel      toChannel = toFile.getChannel();
05 long position = 0;
06 long count = fromChannel.size();
07 toChannel.transferFrom(position, count, fromChannel);

position表示從position處開始向目標文件寫入數據count表示最多傳輸的字節數。若是源通道的剩餘空間小於 count 個字節,則實際傳輸的字節數要小於請求的字節數
此外要注意,在SoketChannel的實現中,SocketChannel只會傳輸此刻準備好的數據可能不足count字節)。所以,SocketChannel可能不會將請求的全部數據(count個字節)所有傳輸到FileChannel中

transferTo()      FileChannel到其餘Channel

transferTo()方法將數據從FileChannel傳輸到其餘的channel中

 
01 RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt""rw");
02 FileChannel      fromChannel = fromFile.getChannel();
03 RandomAccessFile toFile = new RandomAccessFile("toFile.txt""rw");
04 FileChannel      toChannel = toFile.getChannel();
05 long position = 0;
06 long count = fromChannel.size();
07 fromChannel.transferTo(position, count, toChannel);

transferTo()和transferFrom()特別類似,除了調用方法的FileChannel對象不同外,其餘的都同樣
上面所說的關於SocketChannel問題在transferTo()方法中一樣存在。SocketChannel會一直傳輸數據直到目標buffer被填滿

 

Selector

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

爲何使用Selector

僅用單個線程處理多個Channel的好處是,只須要更少的線程來處理通道。事實上,能夠只用一個線程處理全部的通道。對於操做系統來講,線程之間上下文切換的開銷很大,並且每一個線程都要佔用系統的一些資源(如內存)。所以,使用的線程越少越好

單線程使用一個Selector處理3個channel

Selector的建立

經過調用Selector.open()方法建立一個Selector

1 Selector selector = Selector.open();

向Selector註冊通道

爲了將Channel和Selector配合使用,必須將channel註冊到selector上。經過Channel.register()方法來實現

1 channel.configureBlocking(false);
2 SelectionKey key = channel.register(selector,Selectionkey.OP_READ);

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

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

  1. Connect
  2. Accept
  3. Read
  4. Write

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

這四種事件用SelectionKey的四個常量來表示

  1. SelectionKey.OP_CONNECT
  2. SelectionKey.OP_ACCEPT
  3. SelectionKey.OP_READ
  4. SelectionKey.OP_WRITE

若是你對不止一種事件感興趣,那麼能夠用「位或」操做符將常量鏈接起來,以下:

1 int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

SelectionKey

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

  • interest集合
  • ready集合
  • Channel
  • Selector
  • 附加的對象(可選)

interest集合

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

1 int interestSet = selectionKey.interestOps();
2 boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
3 boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
4 boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
5 boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;

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

ready集合

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

1 int readySet = selectionKey.readyOps();

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

1 selectionKey.isAcceptable();
2 selectionKey.isConnectable();
3 selectionKey.isReadable();
4 selectionKey.isWritable();

Channel + Selector

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

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

附加的對象

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

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

還能夠在用register()方法向Selector註冊Channel的時候附加對象。如:

1 SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

經過Selector選擇通道

一旦向Selector註冊了一或多個通道,就能夠調用幾個重載的select()方法。這些方法返回你所感興趣的事件(如鏈接、接受、讀或寫已經準備就緒的那些通道。換句話說,若是你對「讀就緒」的通道感興趣select()方法會返回讀事件已經就緒的那些通道

下面是select()方法

  • int select()
  • int select(long timeout)
  • int selectNow()

select()阻塞到至少一個通道在註冊的事件上就緒

select(long timeout)和select()同樣,除了最長會阻塞timeout毫秒(參數)

selectNow()不會阻塞無論什麼通道就緒都馬上返回此方法執行非阻塞的選擇操做。若是自從前一次選擇操做後沒有通道變成可選擇的,則此方法直接返回零

select()方法返回的int值表示有多少通道已經就緒。亦即,自上次調用select()方法後有多少通道變成就緒狀態。若是調用select()方法,由於有一個通道變成就緒狀態,返回了1,若再次調用select()方法,若是另外一個通道就緒了,它會再次返回1。若是對第一個就緒的channel沒有作任何操做,如今就有兩個就緒的通道,但在每次select()方法調用之間,只有一個通道就緒了

selectedKeys()

一旦調用select()方法,而且返回值代表有一個或更多個通道就緒了,而後能夠經過調用selector的selectedKeys()方法,訪問已選擇鍵集(selected key set)」中的就緒通道。以下所示

1 Set selectedKeys = selector.selectedKeys();

當向Selector註冊Channel,Channel.register()方法會返回一個SelectionKey 對象。這個對象表明了註冊到該Selector的通道。能夠經過SelectionKey的selectedKeySet()方法訪問這些對象

能夠遍歷這個已選擇的鍵集合來訪問就緒的通道。以下

01 Set selectedKeys = selector.selectedKeys();
02 Iterator keyIterator = selectedKeys.iterator();
03 while(keyIterator.hasNext()) {
04     SelectionKey key = keyIterator.next();
05     if(key.isAcceptable()) {
06         // a connection was accepted by a ServerSocketChannel.
07     else if (key.isConnectable()) {
08         // a connection was established with a remote server.
09     else if (key.isReadable()) {
10         // a channel is ready for reading
11     else if (key.isWritable()) {
12         // a channel is ready for writing
13     }
14     keyIterator.remove();
15 }

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

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

SelectionKey.channel()方法返回的通道須要轉型成你要處理的類型,如ServerSocketChannel或SocketChannel等

wakeUp()

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

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

close()

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

完整的示例

打開一個Selector,註冊一個通道到這個Selector上(通道的初始化過程略去),而後持續監控這個Selector的四種事件(接受,鏈接,讀,寫)是否就緒

 
01 Selector selector = Selector.open();
02 channel.configureBlocking(false);
03 SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
04 while(true) {
05   int readyChannels = selector.select();
06   if(readyChannels == 0continue;
07   Set selectedKeys = selector.selectedKeys();
08   Iterator keyIterator = selectedKeys.iterator();
09   while(keyIterator.hasNext()) {
10     SelectionKey key = keyIterator.next();
11     if(key.isAcceptable()) {
12         // a connection was accepted by a ServerSocketChannel.
13     else if (key.isConnectable()) {
14         // a connection was established with a remote server.
15     else if (key.isReadable()) {
16         // a channel is ready for reading
17     else if (key.isWritable()) {
18         // a channel is ready for writing
19     }
20     keyIterator.remove();
21   }
 

IO與NIO的區別

IO NIO
面向流 面向緩衝(塊)
阻塞IO 非阻塞IO
選擇器

 

 

 

 

面向流VS面向緩衝

IO面向流,NIO面向緩衝區

面向流意味着每次從流中讀一個或多個字節,直至讀取全部字節,它們沒有被緩存在任何地方。此外,它不能先後移動流中的數據若是須要先後移動從流中讀取的數據,須要先將它緩存到一個緩衝區

NIO的緩衝導向方法略有不一樣。數據讀取到一個它稍後處理的緩衝區,須要時可在緩衝區中先後移動。這就增長了處理過程當中的靈活性。可是,還須要檢查是否該緩衝區中包含全部須要處理的數據。並且,需確保當更多的數據讀入緩衝區時不要覆蓋緩衝區裏還沒有處理的數據

阻塞VS非阻塞

IO的各類流是阻塞的,當一個線程調用read() 或 write()時,該線程阻塞直到有一些數據被讀取,或數據徹底寫入該線程在此期間不能再幹任何事情

NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,可是它僅能獲得目前可用的數據若是目前沒有數據可用時,就什麼都不會獲取,而不是保持線程阻塞,因此直至數據變的能夠讀取以前,該線程能夠繼續作其餘的事情。 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不須要等待它徹底寫入,這個線程同時能夠去作別的事情。 線程一般將非阻塞IO的空閒時間用於在其它通道上執行IO操做,因此一個單獨的線程如今能夠管理多個輸入和輸出通道(channel)

阻塞IO通信模型

阻塞I/O在調用InputStream.read()方法時是阻塞的,它會一直等到數據到來時(或超時)纔會返回;一樣,在調用ServerSocket.accept()方法時,也會一直阻塞到有客戶端鏈接纔會返回每一個客戶端鏈接過來後,服務端都會啓動一個線程處理該客戶端的請求

阻塞I/O缺點

1. 當客戶端很是多時,會建立大量的處理線程。且每一個線程都要佔用棧空間和一些CPU時間

2. 阻塞可能帶來頻繁的上下文切換,且大部分上下文切換多是無心義的

非阻塞IO通信模型

1. 由一個專門的線程處理全部的 IO 事件,並負責分發
2. 事件驅動機制:事件到的時候觸發,而同步的去監視事件
3. 線程通信:線程之間經過 wait,notify 等方式通信。保證每次上下文切換都是有意義的。減小無謂的線程切換

NIO的通信模型如何實現

NIO採用了雙向通道(channel)進行數據傳輸,而不是單向的流(stream),在通道上能夠註冊咱們感興趣的事件。一共有如下種事件

事件名 對應值
服務端接收客戶端鏈接事件 SelectionKey.OP_ACCEPT(16)
客戶端鏈接服務端事件 SelectionKey.OP_CONNECT(8)
讀事件 SelectionKey.OP_READ(1)
寫事件 SelectionKey.OP_WRITE(4)

      

服務端客戶端各自維護一個管理通道的對象,咱們稱之爲selector,該對象能檢測一個或多個通道 (channel) 上的事件。以服務端爲例,若是服務端的selector上註冊了讀事件,某時刻客戶端給服務端發送了一些數據阻塞I/O這時會調用read()方法阻塞地讀取數據,而NIO的服務端會在selector中添加一個讀事件。服務端的處理線程輪詢地訪問selector,若是訪問selector時發現有感興趣的事件到達,則處理這些事件,若是沒有感興趣的事件到達,則處理線程會一直阻塞直到感興趣的事件到達爲止

 

NIO的選擇器容許一個線程監視多個輸入通道,你能夠註冊多個通道使用一個選擇器,而後使用一個單獨的線程來「選擇」通道:這些通道里已經有能夠處理的輸入,或者選擇已準備寫入的通道。這種選擇機制,使得一個單獨的線程很容易來管理多個通道

IO[逐字節]和NIO底層原理的區別

IO設計中,咱們從InputStream或 Reader逐字節讀取數據。假設正在處理基於行的文本數據流,例如

Name: Anna 
Age: 25
Email: anna@mailserver.com 
Phone: 1234567890 

該文本行的流能夠這樣處理

InputStream input = ... ; // get the InputStream from the client socket   
BufferedReader reader = new BufferedReader(new InputStreamReader(input));   
String nameLine   = reader.readLine(); 
String ageLine    = reader.readLine(); 
String emailLine  = reader.readLine(); 
String phoneLine  = reader.readLine(); 

處理狀態由程序執行多久決定。換句話說,一旦reader.readLine()方法返回,你就知道確定文本行已讀完, readLine()阻塞直到整行讀完。一旦正在運行的線程已處理過讀入的某些數據,該線程不會再回退數據

 

NIO的實現會有所不一樣,下面是一個簡單的例子

ByteBuffer buffer = ByteBuffer.allocate(48); 
int bytesRead = inChannel.read(buffer); 

注意第二行,從通道讀取字節到ByteBuffer。當這個方法調用返回時不知道你所需的全部數據是否在緩衝區內。你所知道的是,該緩衝區包含一些字節,這使得處理有點困難。假設第一次 read(buffer)調用後,讀入緩衝區的數據只有半行,例如,「Name:An」,你能處理數據嗎顯然不能須要等待直到整行數據讀入緩存在此以前,對數據的任何處理毫無心義。因此,你怎麼知道是否緩衝區包含足夠的數據能夠處理呢?好了,你不知道。發現的方法只能查看緩衝區中的數據。其結果是,在你知道全部數據都在緩衝區裏以前,你必須檢查幾回緩衝區的數據。這不只效率低下,並且可使程序設計方案雜亂不堪。例如

ByteBuffer buffer = ByteBuffer.allocate(48);   
int bytesRead = inChannel.read(buffer);   
while(! bufferFull(bytesRead) ) {   
       bytesRead = inChannel.read(buffer);   
} 

bufferFull()方法跟蹤有多少數據讀入緩衝區,並返回truefalse,這取決於緩衝區是否已滿,若是緩衝區已滿,它能夠被處理

 

NIO只使用一個(或幾個)單線程管理多個通道(網絡鏈接或文件),但付出的代價解析數據可能會比從一個阻塞流中讀取數據更復雜

若是須要管理同時打開的成千上萬個鏈接,這些鏈接每次只是發送少許的數據,例如聊天服務器,實現NIO的服務器多是一個優點。一樣,若是你須要維持多個打開的鏈接到其餘計算機上,如P2P網絡中,使用一個單獨的線程來管理你全部出站鏈接,多是一個優點

一個線程多個鏈接的設計方案以下圖所示

若是有少許鏈接使用很是帶寬,一次發送大量的數據,也許典型的IO服務器實現可能很是契合。下圖說明了一個典型的IO服務器設計

FileChannel

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

使用FileChannel之,必須先打開它。可是,咱們沒法直接打開一個FileChannel,須要經過使用一個InputStream、OutputStream或RandomAccessFile獲取一個FileChannel實例

1 RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt""rw");
2 FileChannel inChannel = aFile.getChannel();

size()方法將返回該實例所關聯文件的大小

1 long fileSize = channel.size();

truncate()方法截取一個文件。截取文件時,文件中指定長度後面的部分將被刪除

1 channel.truncate(1024);

這個例子截取文件的前1024個字節

 

FileChannel的某個特定位置進行數據的讀/寫操做。能夠經過調用position()方法獲取FileChannel的當前位置

也能夠經過調用position(long pos)方法設置FileChannel的當前位置

1 long pos = channel.position();
2 channel.position(pos +123);

若是將位置設置在文件結束符以後,而後試圖從文件通道中讀取數據,讀方法將返回-1 —— 文件結束標誌

若是將位置設置在文件結束符以後,而後向通道中寫數據文件撐大到當前位置並寫入數據。這可能致使「文件空洞」,磁盤上物理文件中寫入的數據間有空隙

 

SocketChannel

SocketChannel是一個鏈接到TCP網絡套接字的通道。能夠經過如下2種方式建立SocketChannel:

  1. 打開一個SocketChannel並鏈接到互聯網上的某臺服務器
  2. 一個新鏈接到達ServerSocketChannel時,會建立一個SocketChannel

 

參考:

https://www.cnblogs.com/xiaoxi/p/6576588.html

http://www.jb51.net/article/92202.htm

http://ifeve.com/java-nio-all/

相關文章
相關標籤/搜索