天天看點

[NIO日記](2)NIO vs IO

我應該何時使用IO,何時使用NIO呢?在本文中,我會盡量清晰地解析Java NIO和IO的差異、它們的使用場景,以及它們如何影響您的代碼設計。

1. Java NIO和IO的主要差別 

下表總結了Java NIO和IO之間的主要差别,我會更詳細地描述表中每部分的差異。

IO NIO
Stream oriented Buffer oriented
Blocking IO Non blocking IO
Selectors

1.1 面向流與面向緩沖 

Java NIO和IO之間第一個最大的差別是,IO是面向流的,NIO是面向緩沖區的。 Java IO面向流意味着每次從流中讀一個或多個位元組,直至讀取所有位元組,它們沒有被緩存在任何地方。此外,它不能前後移動流中的資料。如果需要前後移動從流中讀取的資料,需要先将它緩存到一個緩沖區。

Java NIO的緩沖導向方法略有不同。資料讀取到一個它稍後處理的緩沖區,需要時可在緩沖區中前後移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩沖區中包含所有您需要處理的資料。而且,需確定當更多的資料讀入緩沖區時,不要覆寫緩沖區裡尚未處理的資料。 

1.2 阻塞與非阻塞IO 

Java IO的各種流是阻塞的。這意味着,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些資料被讀取,或資料完全寫入。該線程在此期間不能再幹任何事情了。 Java NIO的非阻塞模式,使一個線程從某通道發送請求讀取資料,但是它僅能得到目前可用的資料,如果目前沒有資料可用時,就什麼都不會擷取。而不是保持線程阻塞,是以直至資料變的可以讀取之前,該線程可以繼續做其他的事情。

非阻塞寫也是如此。一個線程請求寫入一些資料到某通道,但不需要等待它完全寫入,這個線程同時可以去做别的事情。 線程通常将非阻塞IO的空閑時間用于在其它通道上執行IO操作,是以一個單獨的線程現在可以管理多個輸入和輸出通道(channel)。 

1.3 選擇器(Selectors) 

Java NIO的選擇器允許一個單獨的線程來監視多個輸入通道,你可以注冊多個通道使用一個選擇器,然後使用一個單獨的線程來“選擇”通道:這些通道裡已經有可以處理的輸入,或者選擇已準備寫入的通道。這種選擇機制,使得一個單獨的線程很容易來管理多個通道。 

1.4 NIO和IO如何影響應用程式的設計 

無論您選擇IO或NIO工具箱,可能會影響您應用程式設計的以下幾個方面: 

(1)對NIO或IO類的API調用

當然,使用NIO的API調用時看起來與使用IO時有所不同,但這并不意外,因為并不是僅從一個InputStream逐位元組讀取,而是資料必須先讀入緩沖區再處理。 

(2)資料處理 

使用純粹的NIO設計相較IO設計,資料處理也受到影響。 在IO設計中,我們從InputStream或 Reader逐位元組讀取資料。假設你正在處理一基于行的文本資料流,例如: 

  1. Name: Anna

  2. Age: 25

  3. Email: [email protected]

  4. Phone: 1234567890

該文本行的流可以這樣處理:

  1. InputStream input = … ; // get the InputStream from the client socket

  2. BufferedReader reader = new BufferedReader(new InputStreamReader(input));

  3. String nameLine = reader.readLine();

  4. String ageLine = reader.readLine();

  5. String emailLine = reader.readLine();

  6. String phoneLine = reader.readLine();

請注意處理狀态由程式執行多久決定。換句話說,一旦reader.readLine()方法傳回,你就知道肯定文本行就已讀完, readline()阻塞直到整行讀完,這就是原因。你也知道此行包含名稱;同樣,第二個readline()調用傳回的時候,你知道這行包含年齡等。 正如你可以看到,該處理程式僅在有新資料讀入時運作,并知道每步的資料是什麼。一旦正在運作的線程已處理過讀入的某些資料,該線程不會再回退資料(大多如此)。下圖也說明了這條原則:

而一個NIO的實作會有所不同,下面是一個簡單的例子:

  1. ByteBuffer buffer = ByteBuffer.allocate(48);

  2. int bytesRead = inChannel.read(buffer);



注意第二行,從通道讀取位元組到ByteBuffer。當這個方法調用傳回時,你不知道你所需的所有資料是否在緩沖區内。你所知道的是,該緩沖區包含一些位元組,這使得處理有點困難。 

假設第一次 read(buffer)調用後,讀入緩沖區的資料隻有半行,例如,“Name:An”,你能處理資料嗎?顯然不能,需要等待,直到整行資料讀入緩存,在此之前,對資料的任何處理毫無意義。 

是以,你怎麼知道是否該緩沖區包含足夠的資料可以處理呢?好了,你不知道,方法中隻能檢視緩沖區中的資料。其結果是,在你知道所有資料都在緩沖區裡之前,你必須檢查幾次緩沖區的資料。這不僅效率低下,而且可以使程式設計方案雜亂不堪。例如: 

  1. ByteBuffer buffer = ByteBuffer.allocate(48);

  2. int bytesRead = inChannel.read(buffer);

  3. while(! bufferFull(bytesRead) ) {

  4. bytesRead = inChannel.read(buffer);

  5. }

bufferFull()方法必須跟蹤有多少資料讀入緩沖區,并傳回真或假,這取決于緩沖區是否已滿。換句話說,如果緩沖區準備好被處理,那麼表示緩沖區滿了。 

bufferFull()方法掃描緩沖區,但必須保持在bufferFull()方法被調用之前狀态相同。如果沒有,下一個讀入緩沖區的資料可能無法讀到正确的位置。這是不可能的,但卻是需要注意的又一問題。 

如果緩沖區已滿,它可以被處理。如果它不滿,并且在你的實際案例中有意義,你或許能處理其中的部分資料。但是許多情況下并非如此。下圖展示了“緩沖區資料循環就緒”:

             從一個通道裡讀資料,直到所有的資料都讀到緩沖區裡

2. 總結 

NIO可讓您隻使用一個(或幾個)單線程管理多個通道(網絡連接配接或檔案),但付出的代價是解析資料可能會比從一個阻塞流中讀取資料更複雜。 

如果需要管理同時打開的成千上萬個連接配接,這些連接配接每次隻是發送少量的資料,例如聊天伺服器,實作NIO的伺服器可能是一個優勢。同樣,如果你需要維持許多打開的連接配接到其他計算機上,如P2P網絡中,使用一個單獨的線程來管理你所有出站連接配接,可能是一個優勢。

一個線程多個連接配接的設計方案如下圖所示:

如果你有少量的連接配接使用非常高的帶寬,一次發送大量的資料,也許典型的IO伺服器實作可能非常契合。下圖說明了一個典型的IO伺服器設計: