引言:NIO是Java面試中老生常談的一個(gè)話題,No-Block-IO(非阻塞IO);今天?;艘惶鞎r(shí)間將并發(fā)變成網(wǎng)站上便于NIO的東西全看了一遍,下面算是自己的一個(gè)學(xué)習(xí)筆記,方便以后回顧:
一:NIO簡介
1:基本概念nio 是non-blocking的簡稱,在jdk1.4 里提供的新api 。Sun 官方標(biāo)榜的特性如下: 為所有的原始類型提供(Buffer)緩存支持。字符集編碼解碼解決方案。
Channel :一個(gè)新的原始I/O 抽象。 支持鎖和內(nèi)存映射文件的文件訪問接口。 提供多路(non-bloking) 非阻塞式的高伸縮性網(wǎng)絡(luò)I/O 。-
2:幾個(gè)核心類
- A:channel
- B:buffer
- C:selector
2.1:channel:NIO中的通道,類比于JavaIO就相當(dāng)于JavaIO中的流;基本上,所有的 IO 在NIO 中都從一個(gè)Channel 開始。Channel 有點(diǎn)象流。 數(shù)據(jù)可以從Channel讀到Buffer中,也可以從Buffer 寫到Channel中。
2.2:buffer:Buffer 類是 java.nio 的構(gòu)造基礎(chǔ)。一個(gè) Buffer 對(duì)象是固定數(shù)量的數(shù)據(jù)的容器,其作用是一個(gè)存儲(chǔ)器,或者分段運(yùn)輸區(qū),在這里,數(shù)據(jù)可被存儲(chǔ)并在之后用于檢索。緩沖區(qū)可以被寫滿或釋放。對(duì)于每個(gè)非布爾原始數(shù)據(jù)類型都有一個(gè)緩沖區(qū)類,即 Buffer 的子類有:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer 和 ShortBuffer,是沒有 BooleanBuffer 之說的。Java NIO 還有個(gè) MappedByteBuffer,用于表示內(nèi)存映射文件 - 緩沖區(qū)的四個(gè)屬性:
- 1:容量(Capacity):緩沖區(qū)能夠容納的數(shù)據(jù)元素的最大數(shù)量。這一容量在緩沖區(qū)創(chuàng)建時(shí)被設(shè)定,并且永遠(yuǎn)不能被改變。
- 2:上界(Limit):緩沖區(qū)的第一個(gè)不能被讀或?qū)懙脑亍>彌_創(chuàng)建時(shí),limit 的值等于 capacity 的值。假設(shè) capacity = 1024,我們?cè)诔绦蛑性O(shè)置了 limit = 512,說明,Buffer 的容量為 1024,但是從 512 之后既不能讀也不能寫,因此可以理解成,Buffer 的實(shí)際可用大小為 512。
- 3:”位置(Position):下一個(gè)要被讀或?qū)懙脑氐乃饕N恢脮?huì)自動(dòng)由相應(yīng)的 get() 和 put() 函數(shù)更新。
- 4:標(biāo)記(Mark):一個(gè)備忘位置
2.3:selector:允許單線程處理多個(gè) Channel。如果你的應(yīng)用打開了多個(gè)連接(通道),但每個(gè)連接的流量都很低,使用Selector就會(huì)很方便。
overview-selectors.png
二:具體用法:
- 2.1:Buffer的基本用法
使用Buffer讀寫數(shù)據(jù)一般遵循以下四個(gè)步驟: - 1:寫入數(shù)據(jù)到Buffer
- 2:調(diào)用flip()方法
- 3:從Buffer中讀取數(shù)據(jù)
- 4:調(diào)用clear()方法或者compact()方法
當(dāng)向buffer寫入數(shù)據(jù)時(shí),buffer會(huì)記錄下寫了多少數(shù)據(jù)。一旦要讀取數(shù)據(jù),需要通過flip()方法將Buffer從寫模式切換到讀模式。在讀模式下,可以讀取之前寫入到buffer的所有數(shù)據(jù)。
一個(gè)小Demo:
public class deme1Nio {
public static void main(String[] args) {
File file = new File("F:\\1.txt");
try {
RandomAccessFile randomfile = new RandomAccessFile(file, "rw");
FileChannel channel = randomfile.getChannel();
ByteBuffer buff = ByteBuffer.allocate(48);
int data = channel.read(buff);
while(data!=-1){
//去讀到文件的最后位置,安字節(jié)算的;換行:算兩個(gè)字節(jié)
int pos = buff.position();
System.out.println(",pos="+pos);
System.out.println(data);
//切換到讀模式
buff.flip();
while(buff.hasRemaining()){
byte[] bytes = new byte[pos];
buff.get(bytes);
System.out.print(new String(bytes,"GBK"));
}
buff.clear();
data = channel.read(buff);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
- 2.2:聚合和分散:
- 聚合:寫入Channel是指在寫操作時(shí)將多個(gè)buffer的數(shù)據(jù)寫入同一個(gè)Channel,因此,Channel 將多個(gè)Buffer中的數(shù)據(jù)“聚集(gather)”后發(fā)送到Channel。
- 分撒:從Channel中讀取是指在讀操作時(shí)將讀取的數(shù)據(jù)寫入多個(gè)buffer中。因此,Channel將從Channel中讀取的數(shù)據(jù)“分散(scatter)”到多個(gè)Buffer中。
- 應(yīng)用:讀取報(bào)文頭信息和報(bào)文體信息時(shí);
File file1 = new File("f:\\1.txt");
FileInputStream in1;
try {
in1 = new FileInputStream(file1);
FileChannel channel1 = in1.getChannel();
//用來存儲(chǔ)頭部信息
ByteBuffer head = ByteBuffer.allocate(5);
//用來存儲(chǔ)區(qū)body信息
ByteBuffer body = ByteBuffer.allocate(10);
//將兩個(gè)信息整合在一起
ByteBuffer[] arr = {head,body};
long data1 = channel1.read(arr);
while(data1!=-1){
head.flip();
while(head.hasRemaining()){
byte[] he = new byte[head.limit()];
head.get(he);
System.out.println("頭部信息為"+new String(he,"gbk"));
}
body.flip();
while(body.hasRemaining()){
byte[] bo = new byte[body.limit()];
body.get(bo);
System.out.println("內(nèi)容體信息為"+new String(bo,"gbk"));
}
head.compact();
body.compact();
data1 = channel1.read(arr);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void writeCombine(){
File file1 = new File("f:\\2.txt");
RandomAccessFile in1;
try {
in1 = new RandomAccessFile(file1,"rw");
FileChannel channel1 = in1.getChannel();
//用來存儲(chǔ)頭部信息
ByteBuffer head = ByteBuffer.allocate(5);
head.put("99999".getBytes());
//用來存儲(chǔ)區(qū)body信息
ByteBuffer body = ByteBuffer.allocate(10);
body.put("1111111111".getBytes());
//將兩個(gè)信息整合在一起
ByteBuffer[] arr = {head,body};
head.flip();
body.flip();
channel1.write(arr);
channel1.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
- 2.3:通道之間傳輸數(shù)據(jù):
- transfromFrom()和transfromTo()方法的使用;
- FileChannel的transferFrom()方法可以將數(shù)據(jù)從源通道傳輸?shù)紽ileChannel中;
- transferTo()方法將數(shù)據(jù)從FileChannel傳輸?shù)狡渌腸hannel中
public class demo3Transform {
public static void trasfromF(){
File file1 = new File("F:\\1.txt");
File file2 = new File("F:\\2.txt");
try {
RandomAccessFile rfile1 = new RandomAccessFile(file1, "rw");
RandomAccessFile rfile2 = new RandomAccessFile(file2, "rw");
FileChannel channel1 = rfile1.getChannel();
FileChannel channel2 = rfile2.getChannel();
long position = channel2.position();
long size = channel2.size();
//將channel2的內(nèi)容復(fù)制到channel1中去;(從1的position的位置開始)
channel1.transferFrom(channel2, 2, size);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void trasfromT(){
File file1 = new File("F:\\1.txt");
File file2 = new File("F:\\2.txt");
try {
RandomAccessFile rfile1 = new RandomAccessFile(file1, "rw");
RandomAccessFile rfile2 = new RandomAccessFile(file2, "rw");
FileChannel channel1 = rfile1.getChannel();
FileChannel channel2 = rfile2.getChannel();
long position = channel2.position();
long size = channel2.size();
//將channel1的內(nèi)容復(fù)制到channel2中去;(從2的position的位置開始,賦值2那么長)
channel1.transferTo(position, size, channel2);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args) {
demo3Transform.trasfromT();
}
}
- 2.4:selector的使用;
僅用單個(gè)線程來處理多個(gè)Channels的好處是,只需要更少的線程來處理通道。事實(shí)上,可以只用一個(gè)線程處理所有的通道。對(duì)于操作系統(tǒng)來說,線程之間上下文切換的開銷很大,而且每個(gè)線程都要占用系統(tǒng)的一些資源(如內(nèi)存)。因此,使用的線程越少越好。
但是,需要記住,現(xiàn)代的操作系統(tǒng)和CPU在多任務(wù)方面表現(xiàn)的越來越好,所以多線程的開銷隨著時(shí)間的推移,變得越來越小了。實(shí)際上,如果一個(gè)CPU有多個(gè)內(nèi)核,不使用多任務(wù)可能是在浪費(fèi)CPU能力。不管怎么說,關(guān)于那種設(shè)計(jì)的討論應(yīng)該放在另一篇不同的文章中。在這里,只要知道使用Selector能夠處理多個(gè)通道就足夠了。 - 1:Selector的創(chuàng)建
通過調(diào)用Selector.open()方法創(chuàng)建一個(gè)Selector,如下:
elector selector = Selector.open(); - 2:向Selector注冊(cè)通道
為了將Channel和Selector配合使用,必須將channel注冊(cè)到selector上。通過SelectableChannel.register()方法來實(shí)現(xiàn),如下:- 1:channel.configureBlocking(false);
- 2:SelectionKey key = channel.register(selector,Selectionkey.OP_READ);
與Selector一起使用時(shí),Channel必須處于非阻塞模式下。這意味著不能將FileChannel與Selector一起使用,因?yàn)镕ileChannel不能切換到非阻塞模式。而套接字通道都可以。
- 3:SelectionKey
在上一小節(jié)中,當(dāng)向Selector注冊(cè)Channel時(shí),register()方法會(huì)返回一個(gè)SelectionKey對(duì)象。這個(gè)對(duì)象包含了一些屬性:- interest集合
- ready集合
- Channel
- Selector
- 附加的對(duì)象(可選)
下面我會(huì)描述這些屬性。 - interest集合
可以通過SelectionKey讀寫interest集合 - ready集合
ready 集合是通道已經(jīng)準(zhǔn)備就緒的操作的集合。在一次選擇(Selection)之后,你會(huì)首先訪問這個(gè)ready的set集合。 - 附加的對(duì)象
可以將一個(gè)對(duì)象或者更多信息附著到SelectionKey上,這樣就能方便的識(shí)別某個(gè)給定的通道。 - 通過Selector選擇通道
一旦向Selector注冊(cè)了一或多個(gè)通道,就可以調(diào)用幾個(gè)重載的select()方法。這些方法返回你所感興趣的事件(如連接、接受、讀或?qū)懀┮呀?jīng)準(zhǔn)備就緒的那些通道。換句話說,如果你對(duì)“讀就緒”的通道感興趣,select()方法會(huì)返回讀事件已經(jīng)就緒的那些通道。
下面是select()方法:- int select()
- int select(long timeout)
- int selectNow()
- select()阻塞到至少有一個(gè)通道在你注冊(cè)的事件上就緒了。
- select(long timeout)和select()一樣,除了最長會(huì)阻塞timeout毫秒(參數(shù))。
- selectNow()不會(huì)阻塞,不管什么通道就緒都立刻返回
select()方法返回的int值表示有多少通道已經(jīng)就緒
- selectedKeys()
一旦調(diào)用了select()方法,并且返回值表明有一個(gè)或更多個(gè)通道就緒了,然后可以通過調(diào)用selector的selectedKeys()方法,訪問“已選擇鍵集(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();
}
四:IO和NIO
1:區(qū)別:

IO和NIO的區(qū)別.png
