摘要:NIO即New IO,這個庫是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但實現方式不同,NIO主要用到的是塊,所以NIO的效率要比IO高很多。
本文分享自華為雲社區《java中的NIO和IO到底是什么區別?20個問題告訴你答案【奔跑吧!JAVA】》,原文作者:breakDraw 。
NIO即New IO,這個庫是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但實現方式不同,NIO主要用到的是塊,所以NIO的效率要比IO高很多。
Q: NIO和標准IO有什么區別?
A:
- 標准IO, 基於字節流和字符流進行操作,阻塞IO。
- NIO基於通道channel和緩沖區Buffer進行操作,支持非阻塞IO,提供選擇器
§ JavaNIO核心3組件:
§ Channels 通道
Q: 通道Channel對象能同時做讀寫操作嗎?
還是說需要像標准IO那樣,需要同時創建input和output對象才能做讀寫操作?
A:通道Channel是雙向的, 既可以從channel中讀數據,也可以寫數據。
可以看到既能調用read也能調用write,且需要依賴緩沖區buffer。
FileChannel fileChannel = FileChannel.open(new File("a.txt").toPath()); ByteBuffer buf = ByteBuffer.allocate(1024); fileChannel.read(buf); fileChannel.write(buf);
- 注意上圖上,fileChannel.read(buf)是將a.txt里的數據讀到buf, 即a.txt->buf
- fileChannel.write(buf)是將buf里的數據寫入到a.txt中, 即buf->a.txt,不要搞反啦!
- 通道和緩沖區的關系
Q: 通道支持異步讀寫嗎
A: 支持。
Q: 通道的讀寫是否必須要依賴緩沖區buffer?
A: 一般都是依賴buffer的。 但也支持2個管道之間的傳輸,即管道之間直接讀寫。
String[] arr=new String[]{"a.txt","b.txt"}; FileChannel in=new FileInputStream(arr[0]).getChannel(); FileChannel out =new FileOutputStream(arr[1]).getChannel(); // 將a.txt中的數據直接寫進b.txt中,相當於文件拷貝 in.transferTo(0, in.size(), out);
常用的幾種Channel
- FileChannel
Java NIO中的FileChannel是一個連接到文件的通道。可以通過文件通道讀寫文件。FileChannel無法設置為非阻塞模式,它總是運行在阻塞模式下
創建方式
RandomAccessFile file = new RandomAccessFile("D:/aa.txt"); FileChannel fileChannel = file.getChannel();
- SocketChannel
Java NIO中的SocketChannel是一個連接到TCP網絡套接字的通道。支持非阻塞模式socketChannel.configureBlocking(false)。可以通過以下2種方式創建SocketChannel:
打開一個SocketChannel並連接到互聯網上的某台服務器。一個新連接到達ServerSocketChannel時,會創建一個SocketChannel
創建方式
SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("192.168.1.100",80));
- ServerSocketChannel
Java NIO中的 ServerSocketChannel 是一個可以監聽新進來的TCP連接的通道, 就像標准IO中的ServerSocket一樣。ServerSocketChannel類在 java.nio.channels包中。SocketChannel和ServerSocketChannel的區別: 前者用於客戶端,后者用於服務端
創建方式:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket.bind(new InetSocketAddress(80)); serverSocketChannel.configureBlocking(false); while(true){ SocketChannel socketChannel = serverSocketChannel.accept(); if(socketChannle != null) doSomething... }
Buffer緩沖區
- 我們真正要把數據拿到或者要寫數據, 實際上都是通過buffer進行操作的。
文件 <-> buffer <-> 數據 - buffer是1個即可讀也可寫的緩沖區,擁有讀寫2種模式。
- buffer的capacity屬性限定了每個buffer的最大容量,下面的1024就是capacity。
ByteBuffer buf = ByteBuffer.allocate(1024);
- buffer擁有1個position屬性,表示當前的讀寫位置。
- 往buffer中寫數據時,position就會增加。
- position最大值為capacity-1
- 把fileChannel對應文件里的數據 寫入到buffer,叫做寫模式
- 寫之后,調用flip,讓buffer的postion置0,此時相當於准備讀取buffer里的數據(即調用buffer.get()拿數據)
- (這個模式的叫法個人也覺得不太好,很容易繞,你可以就記憶成: flip就是從寫模式轉成讀模式!)
Q: buffer調用flip()方法從寫模式切換到讀模式時,position會變成多少?
A: 變為0。
ByteBuffer buf = ByteBuffer.allocate(1024); // 數據讀到buf中,並返回數量,每次最多讀1024個 int byteRead = fileChannel.read(buf); // 輸出byteRead的數量,最多為1024 System.out.println("position=" + buf.position()+", byteRead=" + byteRead); buf.flip(); // 切換到讀模式了,輸出0 System.out.println("position=" + buf.position());
- buffer擁有1個limit屬性。
- 寫模式下,buffer的limit就是buffer的capacity。
Q: 當buffer從寫模式切換到讀模式時,limit為多少?
A: 每次切換前都要調用flip(),切換后,limit為寫模式中的position。
int byteRead = fileChannel.read(buf); // 輸出1024 System.out.println("limit=" + buf.limit() + ",postion=" + buf.position()); System.out.println("切換到讀模式"); buf.flip(); // 輸出byteRead數量 System.out.println("limit=" + buf.limit());
結果如下
Q: 向buf緩沖區寫數據的方式有哪些?
A:
- int byteRead = fileChannel.read(buf);
從通道中讀數據到buf中, 即相當於向buf緩沖區中寫數據。 - buf.putChar(‘a’);
手動向buf中寫入字符a, postion加1。
Q: 從buf緩沖區讀數據的方式有哪些?
- int bytesWrite = fileChannel.write(buf)
buf中的數據寫入到管道,即相當於fileChannel讀取buf中的數據。 - byte getByte = buf.get()
手動讀取1個buf中的字符,postion加1.
Q: 手動修改當前緩沖區的postion的方法有哪些?
A:
- rewind() 將postion設置為0
- mark() 可以標記1個特定的位置, 相當於打標記, 在一頓操作后,可通過reset()回到之前mark()的位置(就像你需要mark我的這幾篇博文一樣!)
Q:1個channel管道支持多個buffer嗎?
A: 支持。 通道的write和read方法都支持傳入1個buffer數組,會按照順序做讀寫操作。
Buffer的種類:
Buffer的另外3個方法:
- warp:
根據一個byte[]來生成一個固定的ByteBuffer時,使用ByteBuffer.wrap()非法的合適。他會直接基於byte[]數組生成一個新的buffer,值也保持一致。 - slice:
得到切片后的數組。 - duplicate:
調用duplicate方法返回的Buffer對象就是復制了一份原始緩沖區,復制了position、limit、capacity這些屬性 - 注意!!!!!!
以上warp\slice\duplicte生成的緩沖區get和put所操作的數組還是與原始緩沖區一樣的。所以對復制后的緩沖區進行修改也會修改原始的緩沖區,反之亦然。
因此duplicte、slice一般是用於操作一下poistion\limit等處理,但是原內容不會去變他,否則就會引起原緩沖器的修改。
§ Selector
selector可用來在線程中關聯多個通道,並進行事件監聽。
Q: 在NIO中Selector的好處是什么?
A:
- 可以用更少的線程來管理各個通道。
- 減少線程上下文切換的資源開銷。
Q: Selector支持注冊哪種類型的通道?
A:
支持非阻塞的通道。
通道要在注冊前調用 channel.configureBlocking(false) 設置為非阻塞。
例如FileChannel就沒辦法注冊,他注定是阻塞的。而socketChannel就可以支持非阻塞。
Q: Selector注冊時,支持監聽哪幾種事件,對應的常量是什么?(啊最不喜歡記憶這種東西了…)
A:共有4種可監聽事件
- Connect 成功連接到1個服務器,對應常量SelectionKey.OP_CONNECT
- Accept 准備好接收新進入的連接, 對應常量SelectionKey.OP_ACCEPT
- Read, 有數據可讀,對應常量SelectionKey.OP_READ
- Write 接收到往里寫的數據, 對應常量SelectionKey.OP_WRITE
如果希望對該通道監聽多種事件,可以用"|"位或操作符把常量連接起來。
int interestingSet = Selectionkey.OP_READ | Selectionkey.OP_WRITE; Selectionkey key = channel.register(selector,interestingSet)
- SelectionKey鍵表示了一個特定的通道對象和一個特定的選擇器對象之間的注冊關系
Q: Selector維護的SelectionKey集合共有哪幾種?
A:共有三種。
(1)已注冊的所有鍵的集合(Registered key set)
所有與選擇器關聯的通道所生成的鍵的集合稱為已經注冊的鍵的集合。並不是所有注冊過的鍵都仍然有效。這個集合通過keys()方法返回,並且可能是空的。這個已注冊的鍵的集合不是可以直接修改的;試圖這么做的話將引發java.lang.UnsupportedOperationException。
(2)已選擇的鍵的集合(Selected key set)
已注冊的鍵的集合的子集。這個集合的每個成員都是相關的通道被選擇器(在前一個選擇操作中)判斷為已經准備好的,並且包含於鍵的interest集合中的操作。這個集合通過selectedKeys()方法返回(並有可能是空的)。
不要將已選擇的鍵的集合與ready集合弄混了。這是一個鍵的集合,每個鍵都關聯一個已經准備好至少一種操作的通道。每個鍵都有一個內嵌的ready集合,指示了所關聯的通道已經准備好的操作。鍵可以直接從這個集合中移除,但不能添加。試圖向已選擇的鍵的集合中添加元素將拋出java.lang.UnsupportedOperationException。
(3)已取消的鍵的集合(Cancelled key set)
已注冊的鍵的集合的子集,這個集合包含了cancel()方法被調用過的鍵(這個鍵已經被無效化),但它們還沒有被注銷。這個集合是選擇器對象的私有成員,因而無法直接訪問。
注冊之后, 如何使用selector對准備就緒的通道做處理:
- 調用select()方法獲取已就緒的通道,返回的int值表示有多少通道已經就緒
- 從selector中獲取selectedkeys
- 遍歷selectedkeys
- 查看各SelectionKey中 是否有事件就緒了。
- 如果有事件就緒,從key中獲取對應對應管道。做對應處理
類似如下,一般都會啟1個線程來run這個selector監聽的處理:
while(true) { int readyNum = selector.select(); if (readyNum == 0) { continue; } Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> it = selectedKeys.iterator(); while(it.hasNext()) { SelectionKey key = it.next(); if(key.isAcceptable()) { // 接受連接 } else if (key.isReadable()) { // 通道可讀 } else if (key.isWritable()) { // 通道可寫 } it.remove(); } }
Q:select()方法其實是阻塞方法,即調用時會進入等待,直到把所有通道都輪詢完畢。如果希望提前結束select(),有哪些方法?
A:有2個辦法:
wakeup(), 調用后,select()方法立刻返回。
close(), 直接關閉selector。
PS: 之前說NIO是非阻塞IO,但為什么上面卻說select()方法是阻塞的?
- 其實NIO的非阻塞,指的是IO不阻塞,即我們不會卡在read()處,我們會用selector去查詢就緒狀態,如果狀態ok就。
- 而查詢操作是需要時間,因此select()必須要把所有通道都檢查一遍才能告訴結果,因此select這個查詢操作是阻塞的。
§ 其他
Q: 多線程讀寫同一文件時,如何加鎖保證線程安全?
A:使用FileChannel的加鎖功能。
RandomAccessFile randFile = new RandomAccessFile(target, "rw"); FileChannel channel = randFile.getChannel(); // pos和siz決定加鎖區域, shared指定是否是共享鎖 FileLock fileLock = channel.lock(pos , size , shared); if (fileLock!=null) { do(); // 這里簡化了,實際上應該用try-catch fileLock.release(); }
Q: 如果需要讀1個特大文件,可以使用什么緩沖區?
A:使用MappedByteBuffer。
這個緩沖區可以把大文件理解成1個byte數組來訪問(但實際上並沒有加載這么大的byte數組,實際內容放在內存+虛存中)。
主要通過FileChannel.map(模式,起始位置,區域)來生成1個MappedByteBuffer。然后可以用put和get去處理對應位置的byte。
int length = 0x8FFFFFF;//一個byte占1B,所以共向文件中存128M的數據 try (FileChannel channel = FileChannel.open(Paths.get("src/c.txt"), StandardOpenOption.READ, StandardOpenOption.WRITE);) { MappedByteBuffer mapBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, length); for(int i=0;i<length;i++) { mapBuffer.put((byte)0); } for(int i = length/2;i<length/2+4;i++) { //像數組一樣訪問 System.out.println(mapBuffer.get(i)); } }
三種模式:
- MapMode.READ_ONLY(只讀): 試圖修改得到的緩沖區將導致拋出 ReadOnlyBufferException。
- MapMode.READ_WRITE(讀/寫): 對得到的緩沖區的更改會寫入文件,需要調用fore()方法
- MapMode.PRIVATE(專用): 可讀可寫,但是修改的內容不會寫入文件,只是buffer自身的改變。
Q:NIO中ByteBuffer, 該如何根據正確的編碼,轉為對應的CharBuffer
A:利用Charset的decode功能。
ByteBuffer byteBuffer = ...; Charset charset = Charset.forName("UTF-8"); CharBuffer charBuffer = charset.decode(byteBuffer);
如果是CharBuffer轉ByteBuffer, 就用charset.encode。