閱讀更多

編程語言
特約稿件 Java NIO 系列教程  

2014-04-28  編輯 wangguo 評論(81條) 有429099人瀏覽
Java NIO(New IO)是從Java 1.4版本開始引入的一個新的IO API,可以替代標準的Java IO API。本系列教程將有助于你學習和理解Java NIO。感謝并發編程網的翻譯和投遞。

(關注ITeye官微,隨時隨地查看最新開發資訊、技術文章。)

Java NIO提供了與標準IO不同的IO工作方式:

  • Channels and Buffers(通道和緩沖區):標準的IO基于字節流和字符流進行操作的,而NIO是基于通道(Channel)和緩沖區(Buffer)進行操作,數據總是從通道讀取到緩沖區中,或者從緩沖區寫入到通道中。
  • Asynchronous IO(異步IO):Java NIO可以讓你異步的使用IO,例如:當線程從通道讀取數據到緩沖區時,線程還是可以進行其他事情。當數據被寫入到緩沖區時,線程可以繼續處理它。從緩沖區寫入通道也類似。
  • Selectors(選擇器):Java NIO引入了選擇器的概念,選擇器用于監聽多個通道的事件(比如:連接打開,數據到達)。因此,單個的線程可以監聽多個數據通道。
下面就來詳細介紹Java NIO的相關知識。

Java NIO 概述 Top


(本部分原文鏈接,作者:Jakob Jenkov, 譯者:airu,校對:丁一)
Java NIO 由以下幾個核心部分組成:

  • Channels
  • Buffers
  • Selectors
雖然Java NIO 中除此之外還有很多類和組件,但在我看來,Channel,Buffer 和 Selector 構成了核心的API。其它組件,如Pipe和FileLock,只不過是與三個核心組件共同使用的工具類。因此,在概述中我將集中在這三個組件上。其它組件會在單獨的章節中講到。

Channel 和 Buffer

基本上,所有的 IO 在NIO 中都從一個Channel 開始。Channel 有點象流。 數據可以從Channel讀到Buffer中,也可以從Buffer 寫到Channel中。這里有個圖示:



Channel和Buffer有好幾種類型。下面是JAVA NIO中的一些主要Channel的實現:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel
正如你所看到的,這些通道涵蓋了UDP 和 TCP 網絡IO,以及文件IO。

與這些類一起的有一些有趣的接口,但為簡單起見,我盡量在概述中不提到它們。本教程其它章節與它們相關的地方我會進行解釋。

以下是Java NIO里關鍵的Buffer實現:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer
這些Buffer覆蓋了你能通過IO發送的基本數據類型:byte, short, int, long, float, double 和 char。

Java NIO 還有個 Mappedyteuffer,用于表示內存映射文件, 我也不打算在概述中說明。

Selector

Selector允許單線程處理多個 Channel。如果你的應用打開了多個連接(通道),但每個連接的流量都很低,使用Selector就會很方便。例如,在一個聊天服務器中。

這是在一個單線程中使用一個Selector處理3個Channel的圖示:



要使用Selector,得向Selector注冊Channel,然后調用它的select()方法。這個方法會一直阻塞到某個注冊的通道有事件就緒。一旦這個方法返回,線程就可以處理這些事件,事件的例子有如新連接進來,數據接收等。


Java NIO vs. IO Top



(本部分原文地址,作者:Jakob Jenkov,譯者:郭蕾,校對:方騰飛)
當學習了Java NIO和IO的API后,一個問題馬上涌入腦海:

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


Java NIO和IO的主要區別

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

IONIO
Stream orientedBuffer oriented
Blocking IONon blocking IO
  Selectors


面向流與面向緩沖

Java NIO和IO之間第一個最大的區別是,IO是面向流的,NIO是面向緩沖區的。 Java IO面向流意味著每次從流中讀一個或多個字節,直至讀取所有字節,它們沒有被緩存在任何地方。此外,它不能前后移動流中的數據。如果需要前后移動從流中讀取的數據,需要先將它緩存到一個緩沖區。 Java NIO的緩沖導向方法略有不同。數據讀取到一個它稍后處理的緩沖區,需要時可在緩沖區中前后移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩沖區中包含所有您需要處理的數據。而且,需確保當更多的數據讀入緩沖區時,不要覆蓋緩沖區里尚未處理的數據。

阻塞與非阻塞IO

Java IO的各種流是阻塞的。這意味著,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入。該線程在此期間不能再干任何事情了。 Java NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什么都不會獲取。而不是保持線程阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其他的事情。 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。 線程通常將非阻塞IO的空閑時間用于在其它通道上執行IO操作,所以一個單獨的線程現在可以管理多個輸入和輸出通道(channel)。

選擇器(Selectors)

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

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

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

  • 對NIO或IO類的API調用。
  • 數據處理。
  • 用來處理數據的線程數。
API調用

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

數據處理

使用純粹的NIO設計相較IO設計,數據處理也受到影響。

在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()阻塞直到整行讀完,這就是原因。你也知道此行包含名稱;同樣,第二個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()方法必須跟蹤有多少數據讀入緩沖區,并返回真或假,這取決于緩沖區是否已滿。換句話說,如果緩沖區準備好被處理,那么表示緩沖區滿了。

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

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


從一個通道里讀數據,直到所有的數據都讀到緩沖區里


總結

NIO可讓您只使用一個(或幾個)單線程管理多個通道(網絡連接或文件),但付出的代價是解析數據可能會比從一個阻塞流中讀取數據更復雜。

如果需要管理同時打開的成千上萬個連接,這些連接每次只是發送少量的數據,例如聊天服務器,實現NIO的服務器可能是一個優勢。同樣,如果你需要維持許多打開的連接到其他計算機上,如P2P網絡中,使用一個單獨的線程來管理你所有出站連接,可能是一個優勢。一個線程多個連接的設計方案如下圖所示:


單線程管理多個連接


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


一個典型的IO服務器設計:一個連接通過一個線程處理


通道(Channel) Top



(本部分原文鏈接,作者:Jakob Jenkov,譯者:airu,校對:丁一)
Java NIO的通道類似流,但又有些不同:

  • 既可以從通道中讀取數據,又可以寫數據到通道。但流的讀寫通常是單向的。
  • 通道可以異步地讀寫。
  • 通道中的數據總是要先讀到一個Buffer,或者總是要從一個Buffer中寫入。
正如上面所說,從通道讀取數據到緩沖區,從緩沖區寫入數據到通道。如下圖所示:



Channel的實現

這些是Java NIO中最重要的通道的實現:

  • FileChannel:從文件中讀寫數據。
  • DatagramChannel:能通過UDP讀寫網絡中的數據。
  • SocketChannel:能通過TCP讀寫網絡中的數據。
  • ServerSocketChannel:可以監聽新進來的TCP連接,像Web服務器那樣。對每一個新進來的連接都會創建一個SocketChannel。
基本的 Channel 示例

下面是一個使用FileChannel讀取數據到Buffer中的示例:

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {

System.out.println("Read " + bytesRead);
buf.flip();

while(buf.hasRemaining()){
System.out.print((char) buf.get());
}

buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.close();


注意 buf.flip() 的調用,首先讀取數據到Buffer,然后反轉Buffer,接著再從Buffer中讀取數據。下一節會深入講解Buffer的更多細節。



緩沖區(Buffer) Top



(本部分原文鏈接,作者:Jakob Jenkov,譯者:airu,校對:丁一)
Java NIO中的Buffer用于和NIO通道進行交互。如你所知,數據是從通道讀入緩沖區,從緩沖區寫入到通道中的。

緩沖區本質上是一塊可以寫入數據,然后可以從中讀取數據的內存。這塊內存被包裝成NIO Buffer對象,并提供了一組方法,用來方便的訪問該塊內存。

Buffer的基本用法

使用Buffer讀寫數據一般遵循以下四個步驟:

  • 寫入數據到Buffer
  • 調用flip()方法
  • 從Buffer中讀取數據
  • 調用clear()方法或者compact()方法

當向buffer寫入數據時,buffer會記錄下寫了多少數據。一旦要讀取數據,需要通過flip()方法將Buffer從寫模式切換到讀模式。在讀模式下,可以讀取之前寫入到buffer的所有數據。

一旦讀完了所有的數據,就需要清空緩沖區,讓它可以再次被寫入。有兩種方式能清空緩沖區:調用clear()或compact()方法。clear()方法會清空整個緩沖區。compact()方法只會清除已經讀過的數據。任何未讀的數據都被移到緩沖區的起始處,新寫入的數據將放到緩沖區未讀數據的后面。

下面是一個使用Buffer的例子:

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {

  buf.flip();  //make buffer ready for read

  while(buf.hasRemaining()){
      System.out.print((char) buf.get()); // read 1 byte at a time
  }

  buf.clear(); //make buffer ready for writing
  bytesRead = inChannel.read(buf);
}
aFile.close();


Buffer的capacity,position和limit

緩沖區本質上是一塊可以寫入數據,然后可以從中讀取數據的內存。這塊內存被包裝成NIO Buffer對象,并提供了一組方法,用來方便的訪問該塊內存。

為了理解Buffer的工作原理,需要熟悉它的三個屬性:

  • capacity
  • position
  • limit

position和limit的含義取決于Buffer處在讀模式還是寫模式。不管Buffer處在什么模式,capacity的含義總是一樣的。

這里有一個關于capacity,position和limit在讀寫模式中的說明,詳細的解釋在插圖后面。



capacity

作為一個內存塊,Buffer有一個固定的大小值,也叫“capacity”.你只能往里寫capacity個byte、long,char等類型。一旦Buffer滿了,需要將其清空(通過讀數據或者清除數據)才能繼續寫數據往里寫數據。

position

當你寫數據到Buffer中時,position表示當前的位置。初始的position值為0.當一個byte、long等數據寫到Buffer后, position會向前移動到下一個可插入數據的Buffer單元。position最大可為capacity – 1。

當讀取數據時,也是從某個特定位置讀。當將Buffer從寫模式切換到讀模式,position會被重置為0。當從Buffer的position處讀取數據時,position向前移動到下一個可讀的位置。

limit

在寫模式下,Buffer的limit表示你最多能往Buffer里寫多少數據。 寫模式下,limit等于Buffer的capacity。

當切換Buffer到讀模式時, limit表示你最多能讀到多少數據。因此,當切換Buffer到讀模式時,limit會被設置成寫模式下的position值。換句話說,你能讀到之前寫入的所有數據(limit被設置成已寫數據的數量,這個值在寫模式下就是position)

Buffer的類型

Java NIO 有以下Buffer類型:

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

如你所見,這些Buffer類型代表了不同的數據類型。換句話說,就是可以通過char,short,int,long,float 或 double類型來操作緩沖區中的字節。

MappedByteBuffer 有些特別,在涉及它的專門章節中再講。

Buffer的分配

要想獲得一個Buffer對象首先要進行分配。 每一個Buffer類都有一個allocate方法。下面是一個分配48字節capacity的ByteBuffer的例子。

ByteBuffer buf = ByteBuffer.allocate(48);


這是分配一個可存儲1024個字符的CharBuffer:

CharBuffer buf = CharBuffer.allocate(1024);


向Buffer中寫數據

寫數據到Buffer有兩種方式:

  • 從Channel寫到Buffer。
  • 通過Buffer的put()方法寫到Buffer里。

從Channel寫到Buffer的例子

int bytesRead = inChannel.read(buf); //read into buffer.


通過put方法寫Buffer的例子:

buf.put(127);


put方法有很多版本,允許你以不同的方式把數據寫入到Buffer中。例如, 寫到一個指定的位置,或者把一個字節數組寫入到Buffer。 更多Buffer實現的細節參考JavaDoc。

flip()方法

flip方法將Buffer從寫模式切換到讀模式。調用flip()方法會將position設回0,并將limit設置成之前position的值。

換句話說,position現在用于標記讀的位置,limit表示之前寫進了多少個byte、char等 —— 現在能讀取多少個byte、char等。

從Buffer中讀取數據

從Buffer中讀取數據有兩種方式:

  • 從Buffer讀取數據到Channel。
  • 使用get()方法從Buffer中讀取數據。

從Buffer讀取數據到Channel的例子:

//read from buffer into channel.
int bytesWritten = inChannel.write(buf);


使用get()方法從Buffer中讀取數據的例子

byte aByte = buf.get();


get方法有很多版本,允許你以不同的方式從Buffer中讀取數據。例如,從指定position讀取,或者從Buffer中讀取數據到字節數組。更多Buffer實現的細節參考JavaDoc。

rewind()方法

Buffer.rewind()將position設回0,所以你可以重讀Buffer中的所有數據。limit保持不變,仍然表示能從Buffer中讀取多少個元素(byte、char等)。

clear()與compact()方法

一旦讀完Buffer中的數據,需要讓Buffer準備好再次被寫入。可以通過clear()或compact()方法來完成。

如果調用的是clear()方法,position將被設回0,limit被設置成 capacity的值。換句話說,Buffer 被清空了。Buffer中的數據并未清除,只是這些標記告訴我們可以從哪里開始往Buffer里寫數據。

如果Buffer中有一些未讀的數據,調用clear()方法,數據將“被遺忘”,意味著不再有任何標記會告訴你哪些數據被讀過,哪些還沒有。

如果Buffer中仍有未讀的數據,且后續還需要這些數據,但是此時想要先先寫些數據,那么使用compact()方法。

compact()方法將所有未讀的數據拷貝到Buffer起始處。然后將position設到最后一個未讀元素正后面。limit屬性依然像clear()方法一樣,設置成capacity。現在Buffer準備好寫數據了,但是不會覆蓋未讀的數據。

mark()與reset()方法

通過調用Buffer.mark()方法,可以標記Buffer中的一個特定position。之后可以通過調用Buffer.reset()方法恢復到這個position。例如:

buffer.mark();

//call buffer.get() a couple of times, e.g. during parsing.

buffer.reset();  //set position back to mark.


equals()與compareTo()方法

可以使用equals()和compareTo()方法兩個Buffer。

equals()

當滿足下列條件時,表示兩個Buffer相等:

  • 有相同的類型(byte、char、int等)。
  • Buffer中剩余的byte、char等的個數相等。
  • Buffer中所有剩余的byte、char等都相同。

如你所見,equals只是比較Buffer的一部分,不是每一個在它里面的元素都比較。實際上,它只比較Buffer中的剩余元素。

compareTo()方法

compareTo()方法比較兩個Buffer的剩余元素(byte、char等), 如果滿足下列條件,則認為一個Buffer“小于”另一個Buffer:

  • 第一個不相等的元素小于另一個Buffer中對應的元素。
  • 所有元素都相等,但第一個Buffer比另一個先耗盡(第一個Buffer的元素個數比另一個少)。

(譯注:剩余元素是從 position到limit之間的元素)



分散(Scatter)/聚集(Gather) Top


(本部分原文地址,作者:Jakob Jenkov   譯者:郭蕾)

Java NIO開始支持scatter/gather,scatter/gather用于描述從Channel(譯者注:Channel在中文經常翻譯為通道)中讀取或者寫入到Channel的操作。

分散(scatter)從Channel中讀取是指在讀操作時將讀取的數據寫入多個buffer中。因此,Channel將從Channel中讀取的數據“分散(scatter)”到多個Buffer中。

聚集(gather)寫入Channel是指在寫操作時將多個buffer的數據寫入同一個Channel,因此,Channel 將多個Buffer中的數據“聚集(gather)”后發送到Channel。

scatter / gather經常用于需要將傳輸的數據分開處理的場合,例如傳輸一個由消息頭和消息體組成的消息,你可能會將消息體和消息頭分散到不同的buffer中,這樣你可以方便的處理消息頭和消息體。

Scattering Reads

Scattering Reads是指數據從一個channel讀取到多個buffer中。如下圖描述:



代碼示例如下:

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

ByteBuffer[] bufferArray = { header, body };

channel.read(bufferArray);


注意buffer首先被插入到數組,然后再將數組作為channel.read() 的輸入參數。read()方法按照buffer在數組中的順序將從channel中讀取的數據寫入到buffer,當一個buffer被寫滿后,channel緊接著向另一個buffer中寫。

Scattering Reads在移動下一個buffer前,必須填滿當前的buffer,這也意味著它不適用于動態消息(譯者注:消息大小不固定)。換句話說,如果存在消息頭和消息體,消息頭必須完成填充(例如 128byte),Scattering Reads才能正常工作。

Gathering Writes

Gathering Writes是指數據從多個buffer寫入到同一個channel。如下圖描述:



代碼示例如下:

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

//write data into buffers

ByteBuffer[] bufferArray = { header, body };

channel.write(bufferArray);


buffers數組是write()方法的入參,write()方法會按照buffer在數組中的順序,將數據寫入到channel,注意只有position和limit之間的數據才會被寫入。因此,如果一個buffer的容量為128byte,但是僅僅包含58byte的數據,那么這58byte的數據將被寫入到channel中。因此與Scattering Reads相反,Gathering Writes能較好的處理動態消息。



通道之間的數據傳輸 Top



(本部分原文地址,作者:Jakob Jenkov,譯者:郭蕾,校對:周泰)
在Java NIO中,如果兩個通道中有一個是FileChannel,那你可以直接將數據從一個channel(譯者注:channel中文常譯作通道)傳輸到另外一個channel。

transferFrom()

FileChannel的transferFrom()方法可以將數據從源通道傳輸到FileChannel中(譯者注:這個方法在JDK文檔中的解釋為將字節從給定的可讀取字節通道傳輸到此通道的文件中)。下面是一個簡單的例子:

RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel      fromChannel = fromFile.getChannel();

RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel      toChannel = toFile.getChannel();

long position = 0;
long count = fromChannel.size();

toChannel.transferFrom(position, count, fromChannel);


方法的輸入參數position表示從position處開始向目標文件寫入數據,count表示最多傳輸的字節數。如果源通道的剩余空間小于 count 個字節,則所傳輸的字節數要小于請求的字節數。

此外要注意,在SoketChannel的實現中,SocketChannel只會傳輸此刻準備好的數據(可能不足count字節)。因此,SocketChannel可能不會將請求的所有數據(count個字節)全部傳輸到FileChannel中。

transferTo()

transferTo()方法將數據從FileChannel傳輸到其他的channel中。下面是一個簡單的例子:

RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel      fromChannel = fromFile.getChannel();

RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel      toChannel = toFile.getChannel();

long position = 0;
long count = fromChannel.size();

fromChannel.transferTo(position, count, toChannel);


是不是發現這個例子和前面那個例子特別相似?除了調用方法的FileChannel對象不一樣外,其他的都一樣。

上面所說的關于SocketChannel的問題在transferTo()方法中同樣存在。SocketChannel會一直傳輸數據直到目標buffer被填滿。



選擇器(Selector) Top


(本部分原文鏈接,作者:Jakob Jenkov,譯者:浪跡v,校對:丁一)
Selector(選擇器)是Java NIO中能夠檢測一到多個NIO通道,并能夠知曉通道是否為諸如讀寫事件做好準備的組件。這樣,一個單獨的線程可以管理多個channel,從而管理多個網絡連接。

(1)  為什么使用Selector?

僅用單個線程來處理多個Channels的好處是,只需要更少的線程來處理通道。事實上,可以只用一個線程處理所有的通道。對于操作系統來說,線程之間上下文切換的開銷很大,而且每個線程都要占用系統的一些資源(如內存)。因此,使用的線程越少越好。

但是,需要記住,現代的操作系統和CPU在多任務方面表現的越來越好,所以多線程的開銷隨著時間的推移,變得越來越小了。實際上,如果一個CPU有多個內核,不使用多任務可能是在浪費CPU能力。不管怎么說,關于那種設計的討論應該放在另一篇不同的文章中。在這里,只要知道使用Selector能夠處理多個通道就足夠了。

下面是單線程使用一個Selector處理3個channel的示例圖:

(2)  Selector的創建

通過調用Selector.open()方法創建一個Selector,如下:

Selector selector = Selector.open();


(3) 向Selector注冊通道

為了將Channel和Selector配合使用,必須將channel注冊到selector上。通過SelectableChannel.register()方法來實現,如下:

channel.configureBlocking(false);
SelectionKey key = channel.register(selector,
	Selectionkey.OP_READ);


與Selector一起使用時,Channel必須處于非阻塞模式下。這意味著不能將FileChannel與Selector一起使用,因為FileChannel不能切換到非阻塞模式。而套接字通道都可以。

注意register()方法的第二個參數。這是一個“interest集合”,意思是在通過Selector監聽Channel時對什么事件感興趣。可以監聽四種不同類型的事件:

  • Connect
  • Accept
  • Read
  • Write
通道觸發了一個事件意思是該事件已經就緒。所以,某個channel成功連接到另一個服務器稱為“連接就緒”。一個server socket channel準備好接收新進入的連接稱為“接收就緒”。一個有數據可讀的通道可以說是“讀就緒”。等待寫數據的通道可以說是“寫就緒”。

這四種事件用SelectionKey的四個常量來表示:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE
如果你對不止一種事件感興趣,那么可以用“位或”操作符將常量連接起來,如下:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;


在下面還會繼續提到interest集合。

(4)  SelectionKey

在上一小節中,當向Selector注冊Channel時,register()方法會返回一個SelectionKey對象。這個對象包含了一些你感興趣的屬性:

  • interest集合
  • ready集合
  • Channel
  • Selector
  • 附加的對象(可選)
下面我會描述這些屬性。

interest集合

就像向Selector注冊通道一節中所描述的,interest集合是你所選擇的感興趣的事件集合。可以通過SelectionKey讀寫interest集合,像這樣:

int interestSet = selectionKey.interestOps();

boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;


可以看到,用“位與”操作interest 集合和給定的SelectionKey常量,可以確定某個確定的事件是否在interest 集合中。

ready集合

ready 集合是通道已經準備就緒的操作的集合。在一次選擇(Selection)之后,你會首先訪問這個ready set。Selection將在下一小節進行解釋。可以這樣訪問ready集合:

int readySet = selectionKey.readyOps();

可以用像檢測interest集合那樣的方法,來檢測channel中什么事件或操作已經就緒。但是,也可以使用以下四個方法,它們都會返回一個布爾類型:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();


Channel + Selector

從SelectionKey訪問Channel和Selector很簡單。如下:

Channel  channel  = selectionKey.channel();
Selector selector = selectionKey.selector();


附加的對象

可以將一個對象或者更多信息附著到SelectionKey上,這樣就能方便的識別某個給定的通道。例如,可以附加 與通道一起使用的Buffer,或是包含聚集數據的某個對象。使用方法如下:

selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();


還可以在用register()方法向Selector注冊Channel的時候附加對象。如:

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);


(5)  通過Selector選擇通道

一旦向Selector注冊了一或多個通道,就可以調用幾個重載的select()方法。這些方法返回你所感興趣的事件(如連接、接受、讀或寫)已經準備就緒的那些通道。換句話說,如果你對“讀就緒”的通道感興趣,select()方法會返回讀事件已經就緒的那些通道。

下面是select()方法:

  • int select()
  • int select(long timeout)
  • int selectNow()
select()阻塞到至少有一個通道在你注冊的事件上就緒了。

select(long timeout)和select()一樣,除了最長會阻塞timeout毫秒(參數)。

selectNow()不會阻塞,不管什么通道就緒都立刻返回(譯者注:此方法執行非阻塞的選擇操作。如果自從前一次選擇操作后,沒有通道變成可選擇的,則此方法直接返回零。)。

select()方法返回的int值表示有多少通道已經就緒。亦即,自上次調用select()方法后有多少通道變成就緒狀態。如果調用select()方法,因為有一個通道變成就緒狀態,返回了1,若再次調用select()方法,如果另一個通道就緒了,它會再次返回1。如果對第一個就緒的channel沒有做任何操作,現在就有兩個就緒的通道,但在每次select()方法調用之間,只有一個通道就緒了。

selectedKeys()

一旦調用了select()方法,并且返回值表明有一個或更多個通道就緒了,然后可以通過調用selector的selectedKeys()方法,訪問“已選擇鍵集(selected key set)”中的就緒通道。如下所示:

Set selectedKeys = selector.selectedKeys();


當像Selector注冊Channel時,Channel.register()方法會返回一個SelectionKey 對象。這個對象代表了注冊到該Selector的通道。可以通過SelectionKey的selectedKeySet()方法訪問這些對象。

可以遍歷這個已選擇的鍵集合來訪問就緒的通道。如下:

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.<tuihighlight class="tuihighlight"><a href="javascript:;" style="display:inline;float:none;position:inherit;cursor:pointer;color:#7962D5;text-decoration:underline;" onclick="return false;">remove</a></tuihighlight>();
}


這個循環遍歷已選擇鍵集中的每個鍵,并檢測各個鍵所對應的通道的就緒事件。

注意每次迭代末尾的keyIterator.remove()調用。Selector不會自己從已選擇鍵集中移除SelectionKey實例。必須在處理完通道時自己移除。下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中。

SelectionKey.channel()方法返回的通道需要轉型成你要處理的類型,如ServerSocketChannel或SocketChannel等。

(6)  wakeUp()

某個線程調用select()方法后阻塞了,即使沒有通道已經就緒,也有辦法讓其從select()方法返回。只要讓其它線程在第一個線程調用select()方法的那個對象上調用Selector.wakeup()方法即可。阻塞在select()方法上的線程會立馬返回。

如果有其它線程調用了wakeup()方法,但當前沒有線程阻塞在select()方法上,下個調用select()方法的線程會立即“醒來(wake up)”。

(7)  close()

用完Selector后調用其close()方法會關閉該Selector,且使注冊到該Selector上的所有SelectionKey實例無效。通道本身并不會關閉。

(8)  完整的示例

這里有一個完整的示例,打開一個Selector,注冊一個通道注冊到這個Selector上(通道的初始化過程略去),然后持續監控這個Selector的四種事件(接受,連接,讀,寫)是否就緒。

Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
  int readyChannels = selector.select();
  if(readyChannels == 0) continue;
  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.<tuihighlight class="tuihighlight"><a href="javascript:;" style="display:inline;float:none;position:inherit;cursor:pointer;color:#7962D5;text-decoration:underline;" onclick="return false;">remove</a></tuihighlight>();
  }
}


文件通道 Top



(本部分原文鏈接,作者:Jakob Jenkov,譯者:周泰,校對:丁一)
Java NIO中的FileChannel是一個連接到文件的通道。可以通過文件通道讀寫文件。

FileChannel無法設置為非阻塞模式,它總是運行在阻塞模式下。

打開FileChannel

在使用FileChannel之前,必須先打開它。但是,我們無法直接打開一個FileChannel,需要通過使用一個InputStream、OutputStream或RandomAccessFile來獲取一個FileChannel實例。下面是通過RandomAccessFile打開FileChannel的示例:

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();


從FileChannel讀取數據

調用多個read()方法之一從FileChannel中讀取數據。如:

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);


首先,分配一個Buffer。從FileChannel中讀取的數據將被讀到Buffer中。

然后,調用FileChannel.read()方法。該方法將數據從FileChannel讀取到Buffer中。read()方法返回的int值表示了有多少字節被讀到了Buffer中。如果返回-1,表示到了文件末尾。

向FileChannel寫數據

使用FileChannel.write()方法向FileChannel寫數據,該方法的參數是一個Buffer。如:

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {
	channel.write(buf);
}


注意FileChannel.write()是在while循環中調用的。因為無法保證write()方法一次能向FileChannel寫入多少字節,因此需要重復調用write()方法,直到Buffer中已經沒有尚未寫入通道的字節。

關閉FileChannel

用完FileChannel后必須將其關閉。如:

channel.close();


FileChannel的position方法

有時可能需要在FileChannel的某個特定位置進行數據的讀/寫操作。可以通過調用position()方法獲取FileChannel的當前位置。

也可以通過調用position(long pos)方法設置FileChannel的當前位置。

這里有兩個例子:

long pos = channel.position();
channel.position(pos +123);


如果將位置設置在文件結束符之后,然后試圖從文件通道中讀取數據,讀方法將返回-1 —— 文件結束標志。

如果將位置設置在文件結束符之后,然后向通道中寫數據,文件將撐大到當前位置并寫入數據。這可能導致“文件空洞”,磁盤上物理文件中寫入的數據間有空隙。

FileChannel的size方法

FileChannel實例的size()方法將返回該實例所關聯文件的大小。如:

long fileSize = channel.size();


FileChannel的truncate方法

可以使用FileChannel.truncate()方法截取一個文件。截取文件時,文件將中指定長度后面的部分將被刪除。如:

channel.truncate(1024);


這個例子截取文件的前1024個字節。

FileChannel的force方法

FileChannel.force()方法將通道里尚未寫入磁盤的數據強制寫到磁盤上。出于性能方面的考慮,操作系統會將數據緩存在內存中,所以無法保證寫入到FileChannel里的數據一定會即時寫到磁盤上。要保證這一點,需要調用force()方法。

force()方法有一個boolean類型的參數,指明是否同時將文件元數據(權限信息等)寫到磁盤上。

下面的例子同時將文件數據和元數據強制寫到磁盤上:

channel.force(true);


Socket 通道 Top



(本部分原文鏈接,作者:Jakob Jenkov,譯者:鄭玉婷,校對:丁一)
Java NIO中的SocketChannel是一個連接到TCP網絡套接字的通道。可以通過以下2種方式創建SocketChannel:

  • 打開一個SocketChannel并連接到互聯網上的某臺服務器。
  • 一個新連接到達ServerSocketChannel時,會創建一個SocketChannel。
打開 SocketChannel

下面是SocketChannel的打開方式:

SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));


關閉 SocketChannel

當用完SocketChannel之后調用SocketChannel.close()關閉SocketChannel:

socketChannel.close();


從 SocketChannel 讀取數據

要從SocketChannel中讀取數據,調用一個read()的方法之一。以下是例子:

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);


首先,分配一個Buffer。從SocketChannel讀取到的數據將會放到這個Buffer中。

然后,調用SocketChannel.read()。該方法將數據從SocketChannel 讀到Buffer中。read()方法返回的int值表示讀了多少字節進Buffer里。如果返回的是-1,表示已經讀到了流的末尾(連接關閉了)。

寫入 SocketChannel

寫數據到SocketChannel用的是SocketChannel.write()方法,該方法以一個Buffer作為參數。示例如下:

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {
    channel.write(buf);
}


注意SocketChannel.write()方法的調用是在一個while循環中的。Write()方法無法保證能寫多少字節到SocketChannel。所以,我們重復調用write()直到Buffer沒有要寫的字節為止。

非阻塞模式

可以設置 SocketChannel 為非阻塞模式(non-blocking mode).設置之后,就可以在異步模式下調用connect(), read() 和write()了。

connect()

如果SocketChannel在非阻塞模式下,此時調用connect(),該方法可能在連接建立之前就返回了。為了確定連接是否建立,可以調用finishConnect()的方法。像這樣:

socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));

while(! socketChannel.finishConnect() ){
    //wait, or do something else...
}


write()

非阻塞模式下,write()方法在尚未寫出任何內容時可能就返回了。所以需要在循環中調用write()。前面已經有例子了,這里就不贅述了。

read()

非阻塞模式下,read()方法在尚未讀取到任何數據時可能就返回了。所以需要關注它的int返回值,它會告訴你讀取了多少字節。

非阻塞模式與選擇器

非阻塞模式與選擇器搭配會工作的更好,通過將一或多個SocketChannel注冊到Selector,可以詢問選擇器哪個通道已經準備好了讀取,寫入等。Selector與SocketChannel的搭配使用會在后面詳講。



ServerSocket 通道 Top


(本部分原文鏈接,作者:Jakob Jenkov,譯者:鄭玉婷,校對:丁一)
Java NIO中的 ServerSocketChannel 是一個可以監聽新進來的TCP連接的通道,就像標準IO中的ServerSocket一樣。ServerSocketChannel類在 java.nio.channels包中。

這里有個例子:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

serverSocketChannel.socket().bind(new InetSocketAddress(9999));

while(true){
    SocketChannel socketChannel =
            serverSocketChannel.accept();

    //do something with socketChannel...
}


打開 ServerSocketChannel

通過調用 ServerSocketChannel.open() 方法來打開ServerSocketChannel.如:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();


關閉 ServerSocketChannel

通過調用ServerSocketChannel.close() 方法來關閉ServerSocketChannel. 如:

serverSocketChannel.close();


監聽新進來的連接

通過 ServerSocketChannel.accept() 方法監聽新進來的連接。當 accept()方法返回的時候,它返回一個包含新進來的連接的 SocketChannel。因此,accept()方法會一直阻塞到有新連接到達。

通常不會僅僅只監聽一個連接,在while循環中調用 accept()方法. 如下面的例子:

while(true){
    SocketChannel socketChannel =
            serverSocketChannel.accept();

    //do something with socketChannel...
}


當然,也可以在while循環中使用除了true以外的其它退出準則。

非阻塞模式

ServerSocketChannel可以設置成非阻塞模式。在非阻塞模式下,accept() 方法會立刻返回,如果還沒有新進來的連接,返回的將是null。 因此,需要檢查返回的SocketChannel是否是null。如:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);

while(true){
    SocketChannel socketChannel =
            serverSocketChannel.accept();

    if(socketChannel != null){
        //do something with socketChannel...
    }
}


Datagram 通道 Top



(本部分原文鏈接,作者:Jakob Jenkov,譯者:鄭玉婷,校對:丁一)
Java NIO中的DatagramChannel是一個能收發UDP包的通道。因為UDP是無連接的網絡協議,所以不能像其它通道那樣讀取和寫入。它發送和接收的是數據包。

打開 DatagramChannel

下面是 DatagramChannel 的打開方式:

DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));


這個例子打開的 DatagramChannel可以在UDP端口9999上接收數據包。

接收數據

通過receive()方法從DatagramChannel接收數據,如:

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);


receive()方法會將接收到的數據包內容復制到指定的Buffer. 如果Buffer容不下收到的數據,多出的數據將被丟棄。

發送數據

通過send()方法從DatagramChannel發送數據,如:

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();

int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));


這個例子發送一串字符到”jenkov.com”服務器的UDP端口80。 因為服務端并沒有監控這個端口,所以什么也不會發生。也不會通知你發出的數據包是否已收到,因為UDP在數據傳送方面沒有任何保證。

連接到特定的地址

可以將DatagramChannel“連接”到網絡中的特定地址的。由于UDP是無連接的,連接到特定地址并不會像TCP通道那樣創建一個真正的連接。而是鎖住DatagramChannel ,讓其只能從特定地址收發數據。

這里有個例子:

channel.connect(new InetSocketAddress("jenkov.com", 80));


當連接后,也可以使用read()和write()方法,就像在用傳統的通道一樣。只是在數據傳送方面沒有任何保證。這里有幾個例子:

int bytesRead = channel.read(buf);
int bytesWritten = channel.write(but);


管道(Pipe) Top


(本部分原文鏈接,作者:Jakob Jenkov,譯者:黃忠,校對:丁一)
Java NIO 管道是2個線程之間的單向數據連接。Pipe有一個source通道和一個sink通道。數據會被寫到sink通道,從source通道讀取。

這里是Pipe原理的圖示:



創建管道

通過Pipe.open()方法打開管道。例如:

Pipe pipe = Pipe.open();


向管道寫數據

要向管道寫數據,需要訪問sink通道。像這樣:

Pipe.SinkChannel sinkChannel = pipe.sink();


通過調用SinkChannel的write()方法,將數據寫入SinkChannel,像這樣:

String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {
    <b>sinkChannel.write(buf);</b>
}


從管道讀取數據

從讀取管道的數據,需要訪問source通道,像這樣:

Pipe.SourceChannel sourceChannel = pipe.source();


調用source通道的read()方法來讀取數據,像這樣:

ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf);


read()方法返回的int值會告訴我們多少字節被讀進了緩沖區。

  • 大小: 7.6 KB
  • 大小: 9.8 KB
  • 大小: 11.2 KB
  • 大小: 8.9 KB
  • 大小: 17.1 KB


評論 共 81 條
81 樓 xly1981 2019-06-04 10:57
對于阻塞和非阻塞,我理解的一直是將進程從運行狀態調整到等待執行隊列,而不是所謂去干別的事情,這句線程干別的事情總感覺不清不楚的
80 樓 gyyloveyc 2018-03-07 22:02
https://cloud.tencent.com/act/campus/group/detail?group=14954
點此鏈接購買騰訊云服務器,配置“1核2G 1M帶寬  50G云硬盤”,16個月只需120元
購買后通過以下鏈接續費兩年(認證隨便填填就好),120元一年。
https://cloud.tencent.com/act/campus
79 樓 xtg1148 2017-12-16 23:46
  
78 樓 youdianjin 2017-12-08 20:53
按照POSIX標準,IO分為同步IO和異步IO,其中同步IO包括BIO(同步阻塞) 和 NIO (同步非阻塞)。

NIO (同步非阻塞)基于事件驅動機制,實現上采用Reactor模式;

AIO(異步IO),同樣采用事件驅動機制,實現上采用Proactor模式。
77 樓 全站唯一小牙 2017-11-21 09:30
多看看書,學習學習視頻,多敲代碼http://javaee.3g-edu.org/tushu/?lbjeye,,http://javaee.3g-edu.org/shipin/?lbjeye
76 樓 snowstorm 2017-10-25 09:55
上面channel調用transferFrom()方法時,參數順序弄錯了.
75 樓 mfkujjisk 2017-08-10 19:58
阿里巴巴招聘java工程師,大平臺,期待你加入,簡歷請發到67434259@qq.com
74 樓 lws0402 2017-05-05 10:02
he037 寫道
imcrazyforyou 寫道
jaychang 寫道
lzzzl 寫道
全文比較長,想打個比方歸納一下。
原文中說了最重要的3個概念,

Channel 通道

Buffer 緩沖區

Selector 選擇器

其中Channel對應以前的流,Buffer不是什么新東西,Selector是因為nio可以使用異步的非堵塞模式才加入的東西。

以前的流總是堵塞的,一個線程只要對它進行操作,其它操作就會被堵塞,也就相當于水管沒有閥門,你伸手接水的時候,不管水到了沒有,你就都只能耗在接水(流)上。

nio的Channel的加入,相當于增加了水龍頭(有閥門),雖然一個時刻也只能接一個水管的水,但依賴輪換策略,在水量不大的時候,各個水管里流出來的水,都可以得到妥善接納,這個關鍵之處就是增加了一個接水工,也就是Selector,他負責協調,也就是看哪根水管有水了的話,在當前水管的水接到一定程度的時候,就切換一下:臨時關上當前水龍頭,試著打開另一個水龍頭(看看有沒有水)。

當其他人需要用水的時候,不是直接去接水,而是事前提了一個水桶給接水工,這個水桶就是Buffer。也就是,其他人雖然也可能要等,但不會在現場等,而是回家等,可以做其它事去,水接滿了,接水工會通知他們。

這其實也是非常接近當前社會分工細化的現實,也是統分利用現有資源達到并發效果的一種很經濟的手段,而不是動不動就來個并行處理,雖然那樣是最簡單的,但也是最浪費資源的方式。

神一樣的總結!  


總結的太形象生動了,有個小的錯誤點,NIO是同步的


NIO是異步阻塞的

nio是同步非阻塞
73 樓 he037 2017-03-31 19:16
imcrazyforyou 寫道
jaychang 寫道
lzzzl 寫道
全文比較長,想打個比方歸納一下。
原文中說了最重要的3個概念,

Channel 通道

Buffer 緩沖區

Selector 選擇器

其中Channel對應以前的流,Buffer不是什么新東西,Selector是因為nio可以使用異步的非堵塞模式才加入的東西。

以前的流總是堵塞的,一個線程只要對它進行操作,其它操作就會被堵塞,也就相當于水管沒有閥門,你伸手接水的時候,不管水到了沒有,你就都只能耗在接水(流)上。

nio的Channel的加入,相當于增加了水龍頭(有閥門),雖然一個時刻也只能接一個水管的水,但依賴輪換策略,在水量不大的時候,各個水管里流出來的水,都可以得到妥善接納,這個關鍵之處就是增加了一個接水工,也就是Selector,他負責協調,也就是看哪根水管有水了的話,在當前水管的水接到一定程度的時候,就切換一下:臨時關上當前水龍頭,試著打開另一個水龍頭(看看有沒有水)。

當其他人需要用水的時候,不是直接去接水,而是事前提了一個水桶給接水工,這個水桶就是Buffer。也就是,其他人雖然也可能要等,但不會在現場等,而是回家等,可以做其它事去,水接滿了,接水工會通知他們。

這其實也是非常接近當前社會分工細化的現實,也是統分利用現有資源達到并發效果的一種很經濟的手段,而不是動不動就來個并行處理,雖然那樣是最簡單的,但也是最浪費資源的方式。

神一樣的總結!  


總結的太形象生動了,有個小的錯誤點,NIO是同步的


NIO是異步阻塞的
72 樓 andyliu_ad 2016-10-08 13:26
" />" target="_blank">[url][url][url][url][url][url][/url][/url][/url][/url][/url][/url]
    [*]
71 樓 莫欺少年窮Java 2016-04-13 13:32
  
70 樓 Nabulio 2016-04-11 15:38
將的太詳細了
69 樓 imcrazyforyou 2016-04-01 12:47
jaychang 寫道
lzzzl 寫道
全文比較長,想打個比方歸納一下。
原文中說了最重要的3個概念,

Channel 通道

Buffer 緩沖區

Selector 選擇器

其中Channel對應以前的流,Buffer不是什么新東西,Selector是因為nio可以使用異步的非堵塞模式才加入的東西。

以前的流總是堵塞的,一個線程只要對它進行操作,其它操作就會被堵塞,也就相當于水管沒有閥門,你伸手接水的時候,不管水到了沒有,你就都只能耗在接水(流)上。

nio的Channel的加入,相當于增加了水龍頭(有閥門),雖然一個時刻也只能接一個水管的水,但依賴輪換策略,在水量不大的時候,各個水管里流出來的水,都可以得到妥善接納,這個關鍵之處就是增加了一個接水工,也就是Selector,他負責協調,也就是看哪根水管有水了的話,在當前水管的水接到一定程度的時候,就切換一下:臨時關上當前水龍頭,試著打開另一個水龍頭(看看有沒有水)。

當其他人需要用水的時候,不是直接去接水,而是事前提了一個水桶給接水工,這個水桶就是Buffer。也就是,其他人雖然也可能要等,但不會在現場等,而是回家等,可以做其它事去,水接滿了,接水工會通知他們。

這其實也是非常接近當前社會分工細化的現實,也是統分利用現有資源達到并發效果的一種很經濟的手段,而不是動不動就來個并行處理,雖然那樣是最簡單的,但也是最浪費資源的方式。

神一樣的總結!  


總結的太形象生動了,有個小的錯誤點,NIO是同步的
68 樓 imcrazyforyou 2016-04-01 12:39
young7 寫道
lzzzl 寫道
全文比較長,想打個比方歸納一下。
原文中說了最重要的3個概念,

Channel 通道

Buffer 緩沖區

Selector 選擇器

其中Channel對應以前的流,Buffer不是什么新東西,Selector是因為nio可以使用異步的非堵塞模式才加入的東西。

以前的流總是堵塞的,一個線程只要對它進行操作,其它操作就會被堵塞,也就相當于水管沒有閥門,你伸手接水的時候,不管水到了沒有,你就都只能耗在接水(流)上。

nio的Channel的加入,相當于增加了水龍頭(有閥門),雖然一個時刻也只能接一個水管的水,但依賴輪換策略,在水量不大的時候,各個水管里流出來的水,都可以得到妥善接納,這個關鍵之處就是增加了一個接水工,也就是Selector,他負責協調,也就是看哪根水管有水了的話,在當前水管的水接到一定程度的時候,就切換一下:臨時關上當前水龍頭,試著打開另一個水龍頭(看看有沒有水)。

當其他人需要用水的時候,不是直接去接水,而是事前提了一個水桶給接水工,這個水桶就是Buffer。也就是,其他人雖然也可能要等,但不會在現場等,而是回家等,可以做其它事去,水接滿了,接水工會通知他們。

這其實也是非常接近當前社會分工細化的現實,也是統分利用現有資源達到并發效果的一種很經濟的手段,而不是動不動就來個并行處理,雖然那樣是最簡單的,但也是最浪費資源的方式。

這個總結甚至比原文更加精彩!


我看到java官方文檔說,NIO是同步非阻塞的
67 樓 dream_land 2016-03-30 13:04
mark,收藏
66 樓 nick_li 2016-03-03 21:52
總結和翻譯的不錯,看到了我泰哥,啊哈哈
65 樓 renxuegangmen 2016-02-17 14:24
good!!!
64 樓 guliangliang 2015-12-16 20:01
最近也在看java8,一起學習下;感謝樓主分享
63 樓 ly695908698 2015-09-08 15:57
收藏一下···
62 樓 zheyiw 2015-09-04 13:59
61 樓 yawen_feng 2015-08-13 23:24
樓主寫的的確很詳細
60 樓 hopana 2015-07-31 16:38
樓主寫的的確很詳細,@lzzzl 的概括也很精彩!
59 樓 windlike 2015-07-28 14:53
58 樓 likongze 2015-03-28 15:50
樓主寫的好
57 樓 likongze 2015-03-28 15:49
樓主。寫的好
56 樓 隨意而生 2015-03-13 16:43
 channel.configureBlocking(false);
 Set selectedKeys = selector.selectedKeys();
 Iterator keyIterator = selectedKeys.iterator();
 SelectionKey key = keyIterator.next();

以上三行代碼第一行 要注意channel不能是FileChannel
第二行和第三行 應該添加泛型<SelectionKey>,不然第四行處需要進行強制類型轉換才行
55 樓 隨意而生 2015-03-13 16:17
總結的很不錯  贊一個
54 樓 libiao5320 2015-01-19 15:22
頂~~~~~~
53 樓 jaychang 2014-12-28 20:12
lzzzl 寫道
全文比較長,想打個比方歸納一下。
原文中說了最重要的3個概念,

Channel 通道

Buffer 緩沖區

Selector 選擇器

其中Channel對應以前的流,Buffer不是什么新東西,Selector是因為nio可以使用異步的非堵塞模式才加入的東西。

以前的流總是堵塞的,一個線程只要對它進行操作,其它操作就會被堵塞,也就相當于水管沒有閥門,你伸手接水的時候,不管水到了沒有,你就都只能耗在接水(流)上。

nio的Channel的加入,相當于增加了水龍頭(有閥門),雖然一個時刻也只能接一個水管的水,但依賴輪換策略,在水量不大的時候,各個水管里流出來的水,都可以得到妥善接納,這個關鍵之處就是增加了一個接水工,也就是Selector,他負責協調,也就是看哪根水管有水了的話,在當前水管的水接到一定程度的時候,就切換一下:臨時關上當前水龍頭,試著打開另一個水龍頭(看看有沒有水)。

當其他人需要用水的時候,不是直接去接水,而是事前提了一個水桶給接水工,這個水桶就是Buffer。也就是,其他人雖然也可能要等,但不會在現場等,而是回家等,可以做其它事去,水接滿了,接水工會通知他們。

這其實也是非常接近當前社會分工細化的現實,也是統分利用現有資源達到并發效果的一種很經濟的手段,而不是動不動就來個并行處理,雖然那樣是最簡單的,但也是最浪費資源的方式。

神一樣的總結!  
52 樓 flykarry 2014-12-24 17:02
非常棒!!!疑惑點都能得到解答

發表評論

您還沒有登錄,請您登錄后再發表評論

相關推薦

  • JavaNIO chm幫助文檔

    Java NIO系列教程(一) Java NIO 概述 Java NIO系列教程(二) Channel Java NIO系列教程(三) Buffer Java NIO系列教程(四) Scatter/Gather Java NIO系列教程(五) 通道之間的數據傳輸 Java NIO系列教程(六) Selector Java NIO系列教程(七) FileChannel Java NIO系列教程(八) SocketChannel Java NIO系列教程(九) ServerSocketChannel Java NIO系列教程(十) Java NIO DatagramChannel Java NIO系列教程(十一) Pipe Java NIO系列教程(十二) Java NIO與IO

  • Java NIO系列教程(一) Java NIO 概述

    Java NIO系列教程(一) Java NIO 概述

  • Java NIO系列教程

    Java NIO系列教程 Java NIO Channel Buffer Selector SocketChannel

  • java網絡編程精解

    Java網絡編程精解.pdf Java NIO 系列教程.pdf Java NIO (中文版).pdf

  • Java NIO》 (中文版)【完整書簽版】.pdf

    This title provides a complete introduction to this major improvement in the new 1.4 version of Java, the new Java.Nio package. It includes information missing from previous editions of Java that are critical to writing high-performance applications.

  • Java NIO實例

    nio代碼實例,Java NIO 系列教程,買不了吃虧,買不了上當

  • Java+NIO+(中文版).pdf

    Java NIO深入探討了1.4版的I/O新特性,并告訴您如何使用這些特性來極大地提升您所寫的Java代碼的執行效率。這本小冊子就程序員所面臨的有代表性的I/O問題作了詳盡闡述,并講解了如何才能充分利用新的I/O特性所提供的各種潛能。您將通過實例學會如何使用這些工具來解決現實工作中常常遇到的I/O問題,并了解這些新特性如何對響應速率、可伸縮性和可靠性產生直接影響。 NIO API是對1.3版I/O特性的補充而非取代,因此,何時使用新的API,何時老的1.3版I/O API更適合特定應用,也是您將學習的內容。

  • nio視頻教程

    nio視頻教程nio視頻教程nio視頻教程nio視頻教程nio視頻教程nio視頻教程nio視頻教程 個人感覺還不錯,可以看看

  • Java NIO詳解及源碼下載

    博客地址:http://blog.csdn.net/u010156024/article/details/44310709 歡迎訪問。 代碼中詳細講述了Java io流和nio流的用法,可以參考學習。

  • JAVA nio異步長連接服務端與客戶端

    JAVA.NIO 異步長連接客戶端與服務端都有,大家可以看看,另不知道怎么樣將客戶端讀取的BUFF后的數據進行處理可以給出修改嗎?

  • Java NIO (中文版)

    Java NIO 深入探討了 1.4 版的 I/O 新特性,并告訴您如何使用這些特性來極大地提升您所寫的 Java 代碼的執行效率。這本小冊子就程序員所面臨的有代表性的 I/O 問題作了詳盡闡述,并講解了 如何才能充分利用新的 I/O 特性所提供的各種潛能。您將通過實例學會如何使用這些工具來解決現 實工作中常常遇到的 I/O 問題,并了解這些新特性如何對響應速率、可伸縮性和可靠性產生直接影 響。

  • Java IO NIO and NIO 2 無水印pdf

    Java IO NIO and NIO 2 英文無水印pdf pdf所有頁面使用FoxitReader和PDF-XChangeViewer測試都可以打開 本資源轉載自網絡,如有侵權,請聯系上傳者或csdn刪除 本資源轉載自網絡,如有侵權,請聯系上傳者或csdn刪除

  • Java_NIO中文 高清完整版本

    詳細介紹關于 java nio, 以及在各種場景下的使用, 加深以后http2.0以及請求異步化的理解

  • JAVA NIO 按行讀取大文件支持 GB級別-修正版

    本類,是專門為了處理大文件,按行讀取開發的類。 采用讀文件的緩存 fbb 1024*5 行緩存 bb 256 字節 設計思想: 每次通過nio讀取字節到 fbb中 然后對fbb自己中的內容進行行判斷即 10 回車 13 行號 0 文件結束 這樣字節的判斷,然后 返回行 如果 到達 fbb的結尾 還沒有結束,就再通過nio讀取一段字節,繼續處理。 由于對于本程序 116個字節以上的行才有意義,所以 在next實現方法中,有對 116 長度的判斷,否則返回 null 修正了之前版本中的問題: 修正后的方法 private int readByte() throws IOException{ fbb.rewind(); fbb.clear(); if(this.fc.read(fbb)==-1){ EOF=true; return 0;}else{ fbb.flip(); return fbb.limit(); } } 把 fbb.position() 改成 fbb.limit()

  • JAVA NIO 按行讀取大文件,支持 GB級別

    本類,是專門為了處理大文件,按行讀取開發的類。 采用讀文件的緩存 fbb 1024*5 行緩存 bb 256 字節 設計思想: 每次通過nio讀取字節到 fbb中 然后對fbb自己中的內容進行行判斷即 10 回車 13 行號 0 文件結束 這樣字節的判斷,然后 返回行 如果 到達 fbb的結尾 還沒有結束,就再通過nio讀取一段字節,繼續處理。 由于對于本程序 116個字節以上的行才有意義,所以 在next實現方法中,有對 116 長度的判斷,否則返回 null

  • 尚硅谷NIO百度云鏈接

    尚硅谷NIO百度云鏈接

  • JavaNIOpdf書籍

    JavaNIO書籍,很全面,可供IT技術人員以及正在學習的同學提供學習資料,好好珍惜。

  • Java NIO中文版 pdf+world,高清帶書簽

    Java NIO的中文翻譯版本,高清帶書簽pdf,另外還有轉換成了world版。詳細簡述了Java NIO的原理,學Java必看

  • Java IO, NIO and NIO.2(Apress,2015)

    Java I/O, NIO, and NIO.2 is a power-packed book that accelerates your mastery of Java's various I/O APIs. In this book, you'll learn about classic I/O APIs (File, RandomAccessFile, the stream classes and related types, and the reader/writer classes). Next, you'll learn about NIO's buffer, channel, selector, regular expression, charset, and formatter APIs. Finally, you'll discover NIO.2's offerings in terms of an improved file system interface, asynchronous I/O, and the completion of socket channel functionality. After reading and using thi book, you'll gain the accelerated knowledge and skill level to really build applications with efficient data access, especially for today's cloud computing streaming data needs.

  • Java NIO 中英文版 + Pro Java 7 NIO.2

    Java NIO,Ron Hitchens 著,中文版 裴小星 譯,Pro Java 7 NIO.2,Anghel Leonard 著,pdf文字版帶書簽,無安全限制

Global site tag (gtag.js) - Google Analytics 真人娱乐官方网站 清水河县| 金沙县| 郑州市| 富民县| 衡阳市| 苗栗市| 彰化市| 淮北市| 保定市| 增城市| 普宁市| 资兴市| 大连市| 平利县| 大丰市| 长顺县| 平邑县| 西吉县| 那坡县| 济阳县| 兰考县| 海林市| 宁河县| 建水县| 台湾省| 兴隆县| 宁阳县| 乃东县| 吕梁市| 益阳市| 永济市| 杭锦后旗| 伊金霍洛旗| 莱芜市| 离岛区| 沙洋县| 益阳市| 株洲市| 濮阳县| 河西区| 云龙县| 隆德县| 资兴市| 五峰| 惠来县| 乃东县| 巴彦淖尔市| 宜兰县| 马山县| 屯留县| 霸州市| 紫阳县| 渭源县| 庄浪县| 合阳县| 平江县| 宝丰县| 仙居县| 鹿邑县| 合川市| 新蔡县| 阿瓦提县| 固始县| 华宁县| 鹰潭市| 蕲春县| 大余县| 开原市| 佳木斯市| 景宁| 中超| 曲阳县| 西丰县| 东莞市| 天气| 金溪县| 邵阳市| 彭阳县| 嘉善县| 湄潭县| 酒泉市| 伊春市| 城口县| 远安县| 江北区| 博湖县| 虹口区| 阳江市| 济阳县| 云梦县| 太湖县| 孟村| 应城市| 湖北省| 静海县| 崇阳县| 保康县| 周宁县| 台江县| 刚察县| 子长县| 大名县| 宜君县| 阿勒泰市| 五原县| 右玉县| 鄯善县| 泸州市| 延川县| 永寿县| 溧水县| 平邑县| 山东省| 淮阳县| 宜宾市| 庄河市| 丹阳市| 龙口市| 望城县| 吉木乃县| 固原市| 望江县| 香格里拉县| 金阳县| 盘锦市| 义乌市| 临沂市| 定州市| 丹江口市| 阳谷县| 庆阳市| 澎湖县| 商南县| 阿鲁科尔沁旗| 隆安县| 靖州| 沐川县|