NIO即New IO/Non-Blocking IO,JDK1.4中引入。NIO和IO有相同的做用和目的,但實現方式不一樣,NIO主要用到的是塊,因此NIO的效率要比IO高不少html
Java API中提供了兩套NIO,一套是針對標準輸入輸出NIO,另外一套就是網絡編程NIOjava
Java NIO 由如下幾個核心部分組成編程
雖然Java NIO 中除此以外還有不少類和組件,但Channel,Buffer 和 Selector 構成了核心的API。其它組件,如Pipe和FileLock,只不過是與三個核心組件共同使用的工具類數組
基本上,全部的 IO 在NIO 中都從一個Channel 開始。Channel 有點像流。 數據能夠從Channel讀到Buffer中,也能夠從Buffer 寫到Channel中緩存
Channel和Buffer有好幾種類型。下面是JAVA NIO中的一些主要Channel的實現,這些Channel涵蓋了UDP 和 TCP 網絡IO,以及文件IO服務器
與這些類一塊兒的有一些有趣的接口,但爲簡單起見,我儘可能在概述中不提到它們網絡
如下是Java NIO裏關鍵的Buffer實現,這些Buffer覆蓋了你能經過IO發送的基本數據類型:byte, short, int, long, float, double 和 charapp
Java NIO 還有個 MappedByteBuffer,用於表示內存映射文件dom
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) |
Java NIO的通道相似流,但又有些不一樣
FileChannel 從文件中讀寫數據
DatagramChannel 能經過UDP讀寫網絡中的數據
SocketChannel 能經過TCP讀寫網絡中的數據
ServerSocketChannel能夠監聽TCP鏈接,像Web服務器那樣。對每個新進來的鏈接都會建立一個SocketChannel
Buffer用於和Channel進行交互。如你所知,數據是從Channel讀入Buffer,從Buffer寫入到Channel中的
Buffer本質上是一塊可以寫入數據,而後可以從中讀取數據的內存。這塊內存被包裝成NIO Buffer對象,並提供了一組方法,用來方便的訪問該塊內存
使用Buffer讀寫數據通常遵循如下四個步驟
flip()
方法clear()
方法或者compact()
方法當向buffer寫入數據時,buffer會記錄下寫了多少數據。一旦要讀取數據,須要經過flip()方法將Buffer從寫模式切換到讀模式。在讀模式下,能夠讀取以前寫入到buffer的全部數據
一旦讀完了全部的數據,就須要清空緩衝區,讓它可以再次被寫入。有兩種方式能清空緩衝區:調用clear()或compact()方法。clear()方法會清空整個緩衝區。compact()方法只會清除已經讀過的數據,任何未讀的數據都被移到緩衝區的起始處,新寫入的數據將放到緩衝區未讀數據的後面
緩衝區本質上是一塊能夠寫入數據,而後能夠從中讀取數據的內存。這塊內存被包裝成NIO Buffer對象,並提供了一組方法,用來方便的訪問該塊內存
爲了理解Buffer的工做原理,須要熟悉它的三個屬性
position和limit的含義取決於Buffer處在讀模式仍是寫模式。無論Buffer處在什麼模式,capacity的含義老是同樣的
關於capacity,position和limit在讀寫模式中的說明
做爲一個內存塊,Buffer有一個固定的大小值,也叫「capacity」。你只能往裏寫capacity個byte、long,char等類型。一旦Buffer滿了,須要將其清空(經過讀數據或者清除數據)才能繼續往裏寫數據
當你寫數據到Buffer中時,position表示當前的位置。初始的position值爲0。當一個byte、long等數據寫到Buffer後, position會向後移動到下一個可插入數據的Buffer單元。position最大可爲capacity-1
當讀數據時,也是從某個特定位置讀。當將Buffer從寫模式切換到讀模式,position會被重置爲0. 當從Buffer的position處讀取數據時,position向後移動到下一個可讀的位置
在寫模式下,Buffer的limit表示最多能往Buffer裏寫多少數據。 寫模式下,limit等於Buffer的capacity
當切換Buffer到讀模式時, limit表示最多能讀到多少數據。所以,當切換Buffer到讀模式時,limit會被設置成寫模式下的position值。換句話說,你能讀到以前寫入的全部數據(limit被設置成已寫數據的數量,這個值在寫模式下就是position)
要想得到一個Buffer對象首先要進行分配。 每個Buffer類都有一個allocate方法。下面是一個分配48字節capacity的ByteBuffer的例子。
1 |
ByteBuffer buf = ByteBuffer.allocate( 48 ); |
這是分配一個可存儲1024字符的CharBuffer:
1 |
CharBuffer buf = CharBuffer.allocate( 1024 ); |
寫數據到Buffer有兩種方式:
從Channel寫到Buffer的例子
1 |
int bytesRead = inChannel.read(buf); //read into buffer. |
經過put方法寫Buffer的例子
1 |
buf.put( 127 ); |
put方法有不少版本,容許你以不一樣的方式把數據寫入到Buffer中。例如, 寫到一個指定的位置,或者把一個字節數組寫入到Buffer。 更多Buffer實現的細節參考JavaDoc
flip方法將Buffer從寫模式切換到讀模式。調用flip()方法會將position設回0,並將limit設置成以前position的值(寫模式下,當前buffer已經存在數據的長度)
換句話說,position如今用於標記讀的位置,limit表示以前寫進了多少個byte、char等 —— 如今能讀取多少個byte、char等
從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中讀取數據到字節數組
Buffer.rewind()將position設回0,因此能夠重讀Buffer中的全部數據。limit保持不變,仍然表示能從Buffer中讀取多少個元素(byte、char等)
一旦讀完Buffer中的數據,須要讓Buffer準備好再次被寫入。能夠經過clear()或compact()方法來完成
若是調用的是clear()方法,position將被設回0,limit被設置成 capacity的值。換句話說,Buffer 被清空了。Buffer中的數據並未清除,只是這些標記告訴咱們能夠從哪裏開始往Buffer裏寫數據
若是Buffer中有一些未讀的數據,調用clear()方法,數據將「被遺忘」,意味着再也不有任何標記會告訴你哪些數據被讀過,哪些尚未
若是Buffer中仍有未讀的數據,且後續還須要這些數據,可是此時想要先寫些數據,那麼使用compact()方法
compact()方法將全部未讀的數據拷貝到Buffer起始處。而後將position設到最後一個未讀元素後面。limit屬性依然像clear()方法同樣,設置成capacity。如今Buffer準備好寫數據了,可是不會覆蓋未讀的數據
經過調用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()方法比較兩個Buffer
當知足下列條件時,表示兩個Buffer相等
equals只是比較Buffer的一部分,不是每個在它裏面的元素都比較。實際上,它只比較Buffer中的剩餘元素
compareTo()方法比較兩個Buffer的剩餘元素(byte、char等), 若是知足下列條件,則認爲一個Buffer「小於」另外一個Buffer:
剩餘元素是從 position到limit之間的元素
Java NIO支持scatter/gather,scatter/gather用於描述從Channel中讀取或者寫入到Channel的操做
分散(scatter)從Channel中讀取是指在讀操做時將讀取的數據寫入到多個buffer中
彙集(gather)寫入Channel是指在寫操做時將多個buffer的數據寫入同一個Channel
scatter / gather常常用於須要將傳輸的數據分開處理的場合,例如傳輸一個由消息頭和消息體組成的消息,你可能會將消息體和消息頭分散到不一樣的buffer中,這樣你能夠方便的處理消息頭和消息體
Scattering Reads
Scattering Reads是指數據從一個channel讀取到多個buffer中。以下圖描述
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是指數據從多個buffer寫入到同一個channel。以下圖描述
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能較好的處理動態消息
Java NIO中,若是兩個通道中有一個是FileChannel,那你能夠直接將數據從一個channel傳輸到另一個channel
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中
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被填滿
Java NIO中可以檢測一到多個NIO通道,並可以知曉通道是否爲諸如讀寫事件作好準備的組件。這樣,一個單獨的線程能夠管理多個channel,從而管理多個網絡鏈接
僅用單個線程來處理多個Channel的好處是,只須要更少的線程來處理通道。事實上,能夠只用一個線程處理全部的通道。對於操做系統來講,線程之間上下文切換的開銷很大,並且每一個線程都要佔用系統的一些資源(如內存)。所以,使用的線程越少越好
經過調用Selector.open()方法建立一個Selector
1 |
Selector selector = Selector.open(); |
爲了將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時對什麼事件感興趣。能夠監聽四種不一樣類型的事件
通道觸發了一個事件意思是該事件已經就緒。因此,某個channel成功鏈接到另外一個服務器稱爲「鏈接就緒」。一個server socket channel準備好接收新進入的鏈接稱爲「接收就緒」。一個有數據可讀的通道能夠說是「讀就緒」。等待寫數據的通道能夠說是「寫就緒」
這四種事件用SelectionKey的四個常量來表示
若是你對不止一種事件感興趣,那麼能夠用「位或」操做符將常量鏈接起來,以下:
1 |
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE; |
當向Selector註冊Channel時,register()方法會返回一個SelectionKey對象。這個對象包含了一些你感興趣的屬性
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 集合是通道已經準備就緒的操做的集合。在一次選擇(Selection)之後,你會首先訪問這個ready set。能夠這樣訪問ready集合
1 |
int readySet = selectionKey.readyOps(); |
能夠用像檢測interest集合那樣的方法,來檢測channel中什麼事件或操做已經就緒。可是,也可使用如下四個方法,它們都會返回一個布爾類型
1 |
selectionKey.isAcceptable(); |
2 |
selectionKey.isConnectable(); |
3 |
selectionKey.isReadable(); |
4 |
selectionKey.isWritable(); |
從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註冊了一或多個通道,就能夠調用幾個重載的select()方法。這些方法返回你所感興趣的事件(如鏈接、接受、讀或寫)已經準備就緒的那些通道。換句話說,若是你對「讀就緒」的通道感興趣,select()方法會返回讀事件已經就緒的那些通道
下面是select()方法
select()
阻塞到至少有一個通道在你註冊的事件上就緒了
select(long timeout)
和select()同樣,除了最長會阻塞timeout毫秒(參數)
selectNow()
不會阻塞,無論什麼通道就緒都馬上返回(此方法執行非阻塞的選擇操做。若是自從前一次選擇操做後,沒有通道變成可選擇的,則此方法直接返回零)
select()方法返回的int值表示有多少通道已經就緒。亦即,自上次調用select()方法後有多少通道變成就緒狀態。若是調用select()方法,由於有一個通道變成就緒狀態,返回了1,若再次調用select()方法,若是另外一個通道就緒了,它會再次返回1。若是對第一個就緒的channel沒有作任何操做,如今就有兩個就緒的通道,但在每次select()方法調用之間,只有一個通道就緒了
一旦調用了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等
某個線程調用select()方法後阻塞了,即便沒有通道已經就緒,也有辦法讓其從select()方法返回。只要讓其它線程在第一個線程調用select()方法的那個對象上調用Selector.wakeup()方法便可。阻塞在select()方法上的線程會立馬返回
若是有其它線程調用了wakeup()方法,但當前沒有線程阻塞在select()方法上,下個調用select()方法的線程會當即醒來(wake up)
用完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 == 0 ) continue ; |
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 | 非阻塞IO |
無 | 選擇器 |
IO面向流,NIO面向緩衝區
面向流意味着每次從流中讀一個或多個字節,直至讀取全部字節,它們沒有被緩存在任何地方。此外,它不能先後移動流中的數據。若是須要先後移動從流中讀取的數據,須要先將它緩存到一個緩衝區
NIO的緩衝導向方法略有不一樣。數據讀取到一個它稍後處理的緩衝區,須要時可在緩衝區中先後移動。這就增長了處理過程當中的靈活性。可是,還須要檢查是否該緩衝區中包含全部須要處理的數據。並且,需確保當更多的數據讀入緩衝區時,不要覆蓋緩衝區裏還沒有處理的數據
IO的各類流是阻塞的,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據徹底寫入。該線程在此期間不能再幹任何事情
NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,可是它僅能獲得目前可用的數據,若是目前沒有數據可用時,就什麼都不會獲取,而不是保持線程阻塞,因此直至數據變的能夠讀取以前,該線程能夠繼續作其餘的事情。 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不須要等待它徹底寫入,這個線程同時能夠去作別的事情。 線程一般將非阻塞IO的空閒時間用於在其它通道上執行IO操做,因此一個單獨的線程如今能夠管理多個輸入和輸出通道(channel)
阻塞I/O在調用InputStream.read()方法時是阻塞的,它會一直等到數據到來時(或超時)纔會返回;一樣,在調用ServerSocket.accept()方法時,也會一直阻塞到有客戶端鏈接纔會返回,每一個客戶端鏈接過來後,服務端都會啓動一個線程去處理該客戶端的請求
阻塞I/O缺點:
1. 當客戶端很是多時,會建立大量的處理線程。且每一個線程都要佔用棧空間和一些CPU時間
2. 阻塞可能帶來頻繁的上下文切換,且大部分上下文切換多是無心義的
1. 由一個專門的線程來處理全部的 IO 事件,並負責分發
2. 事件驅動機制:事件到的時候觸發,而不是同步的去監視事件
3. 線程通信:線程之間經過 wait,notify 等方式通信。保證每次上下文切換都是有意義的。減小無謂的線程切換
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設計中,咱們從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()方法跟蹤有多少數據讀入緩衝區,並返回true或false,這取決於緩衝區是否已滿,若是緩衝區已滿,它能夠被處理
NIO只使用一個(或幾個)單線程管理多個通道(網絡鏈接或文件),但付出的代價是解析數據可能會比從一個阻塞流中讀取數據更復雜
若是須要管理同時打開的成千上萬個鏈接,這些鏈接每次只是發送少許的數據,例如聊天服務器,實現NIO的服務器多是一個優點。一樣,若是你須要維持多個打開的鏈接到其餘計算機上,如P2P網絡中,使用一個單獨的線程來管理你全部出站鏈接,多是一個優點
一個線程多個鏈接的設計方案以下圖所示
若是有少許的鏈接使用很是高的帶寬,一次發送大量的數據,也許典型的IO服務器實現可能很是契合。下圖說明了一個典型的IO服務器設計
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是一個鏈接到TCP網絡套接字的通道。能夠經過如下2種方式建立SocketChannel:
參考:
https://www.cnblogs.com/xiaoxi/p/6576588.html
http://www.jb51.net/article/92202.htm
http://ifeve.com/java-nio-all/