java中的NIO和IO到底是什麼區別?20個問題告訴你答案

華為雲開發者社群發表於2021-06-08

摘要: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,不要搞反啦!
  • 通道和緩衝區的關係

https://i.iter01.com/images/481b5bf9922ac2ae4b7a01dc41d915f895d0572abe5cec0051d462efce96ed82.png

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());

結果如下

https://i.iter01.com/images/1042058f014aebdcca64ce2c4a71240831aac27a6359687a5b15a171c7bf6672.png

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陣列,會按照順序做讀寫操作。
https://i.iter01.com/images/77fe5bf6262cefb159c9fd6c855c211e71d93b5570b1ee110639c41af9f6a4b8.png

 

Buffer的種類:
https://i.iter01.com/images/e7e17e4f0f2bb0a76e64834e9d8fff962e1d41cba5e282eec3ba9b56e238d968.png

 

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可用來線上程中關聯多個通道,並進行事件監聽。
https://i.iter01.com/images/8fe22604922614cb8ebd875c27b3557f28f5eaba4867595678344035571f64e3.png

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對準備就緒的通道做處理:

  1. 呼叫select()方法獲取已就緒的通道,返回的int值表示有多少通道已經就緒
  2. 從selector中獲取selectedkeys
  3. 遍歷selectedkeys
  4. 檢視各SelectionKey中 是否有事件就緒了。
  5. 如果有事件就緒,從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。

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章