標籤:
轉載請註明出處:jiq?欽‘s technical Blog
引言
JDK1.4中引入了NIO,即New IO,目的在於提高IO速度。特別注意JavaNIO不完全是非阻塞式IO(No-Blocking IO),因為其中部分通道(如FileChannel)只能運行在阻塞模式下,而其他的通道可以在阻塞式和非阻塞式之間進行選擇。
儘管這樣,我們還是習慣將Java NIO看作是非阻塞式IO,而前面介紹的面向流(位元組/字元)的IO類庫則是非阻塞的,詳細來看,兩者區別如下:
IO |
NIO |
面向流(Stream oriented) |
面向緩衝區(Buffer oriented) |
阻塞式(Blocking IO) |
非阻塞式(Non blocking IO) |
無 |
選取器(Selectors) |
但是千萬記住,兩者沒有孰優孰劣,NIO是java io的拓展,根據不同的情境,兩者各有用處。
面向流與面向緩衝
Java NIO和IO之間第一個最大的區別是,IO是面向流的,NIO是面向緩衝區的。 JavaIO面向流意味著每次從流中讀一個或多個位元組,直至讀取所有位元組,它們沒有被緩衝在任何地方。此外,它不能前後移動流中的資料。如果需要前後移動從流中讀取的資料,需要先將它緩衝到一個緩衝區。Java NIO的緩衝導向方法略有不同。資料讀取到一個它稍後處理的緩衝區,需要時可在緩衝區中前後移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩衝區中包含所有您需要處理的資料。而且,需確保當更多的資料讀入緩衝區時,不要覆蓋緩衝區裡尚未處理的資料。
阻塞與非阻塞IO
Java IO的各種流是阻塞的。這意味著,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些資料被讀取,或資料完全寫入。該線程在此期間不能再幹任何事情了。 Java NIO的非阻塞模式,使一個線程從某通道發送請求讀取資料,但是它僅能得到目前可用的資料,如果目前沒有資料可用時,就什麼都不會擷取。而不是保持線程阻塞,所以直至資料變的可以讀取之前,該線程可以繼續做其他的事情。非阻塞寫也是如此。一個線程請求寫入一些資料到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。線程通常將非阻塞IO的空閑時間用於在其它通道上執行IO操作,所以一個單獨的線程現在可以管理多個輸入和輸出通道(channel)。
選取器(Selectors)
Java NIO的選取器允許一個單獨的線程來監視多個輸入通道,你可以註冊多個通道使用一個選取器,然後使用一個單獨的線程來“選擇”通道:這些通道裡已經有可以處理的輸入,或者選擇已準備寫入的通道。這種選擇機制,使得一個單獨的線程很容易來管理多個通道。
Java NIO最關鍵的三個概念分別是通道,緩衝區和選取器:
一、通道(Channel)
Java NIO的通道類似流,但又有些不同:
n 通道是雙向的,可讀也可寫,而流的讀寫是單向的。
n 通道可以非同步地讀寫。
n 無論讀寫,通道只能和Buffer互動。
JavaNIO中最重要的幾個Channel的實現:
u FileChannel:從檔案中讀寫資料。
u DatagramChannel:通過UDP讀寫網路中的資料。
u SocketChannel:通過TCP讀寫網路中的資料。
u ServerSocketChannel:可以監聽新進來的TCP串連,像Web伺服器那樣。對每一個新進來的串連都會建立一個SocketChannel。
下面是一個通過FileChannel來向檔案中寫入資料的例子:
public class Test { public static void main(String[] args) throws IOException { File file = new File("test.txt"); FileOutputStream os = new FileOutputStream(file); FileChannel channel = os.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(1024); String str = "hello,jiyiqin"; buffer.put(str.getBytes()); buffer.flip(); channel.write(buffer); channel.close(); os.close(); } }
備忘:上面樣本有兩個關鍵的地方:一個是通過FileOutputStream檔案輸出資料流擷取通道,舊的IO類庫(或者說面向流的IO類庫)中FileInputStream/FileOutputStream和RandomAccessFile三個類被修改以能夠產生FileChannel通道,但是面向字元的流Reader/Writer不能產生通道。另外在將緩衝區資料寫入通道之前必須要調用緩衝區的flip方法轉換為讀模式,讓通道可從緩衝區讀取資料。
二、緩衝區(Buffer)
Java NIO中的Buffer用於和NIO通道進行互動,資料是從通道讀入緩衝區或從緩衝區寫入到通道中。當向buffer寫入資料時,buffer會記錄下寫了多少資料。一旦要讀取資料,需要通過flip()方法將Buffer從寫入模式切換到讀模式。在讀模式下,可以讀取之前寫入到buffer的所有資料。一旦讀完了所有的資料,就需要清空緩衝區,讓它可以再次被寫入。有兩種方式能清空緩衝區:調用clear()或compact()方法。clear()方法會清空整個緩衝區。compact()方法只會清除已經讀過的資料。任何未讀的資料都被移到緩衝區的起始處,新寫入的資料將放到緩衝區未讀資料的後面。
緩衝區本質上是一塊可以寫入資料,然後可以從中讀取資料的記憶體。這塊記憶體被封裝成NIO Buffer對象,並提供了一組方法,用來方便的訪問該塊記憶體。
下面是一個從檔案通道FileChannel讀取資料的例子:
RandomAccessFile aFile = newRandomAccessFile("data/nio-data.txt", "rw"); FileChannel inChannel =aFile.getChannel(); ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf);while (bytesRead != -1) { buf.flip(); //使緩衝區可讀 while(buf.hasRemaining()){ System.out.print((char)buf.get()); //一次讀取一位元組 } buf.clear(); bytesRead= inChannel.read(buf);}aFile.close();
三、選取器(Selector)
Selector允許單線程處理多個Channel。如果你的應用開啟了多個串連(通道),但每個串連的流量都很低,使用Selector就會很方便。例如,在一個聊天伺服器中。
僅用單個線程來處理多個Channels的好處是,只需要更少的線程來處理通道。事實上,可以只用一個線程處理所有的通道。對於作業系統來說,線程之間環境切換的開銷很大,而且每個線程都要佔用系統的一些資源(如記憶體)。因此,使用的線程越少越好。
但是,需要記住,現代的作業系統和CPU在多任務方面表現的越來越好,所以多線程的開銷隨著時間的推移,變得越來越小了。實際上,如果一個CPU有多個核心,不使用多任務可能是在浪費CPU能力。不管怎麼說,關於那種設計的討論應該放在另一篇不同的文章中。在這裡,只要知道使用Selector能夠處理多個通道就足夠了。
用一張轉載的圖展示在一個單線程中使用一個Selector處理3個Channel:
步驟1:Selector的建立
通過調用Selector.open()方法建立一個Selector,如下:
Selectorselector = Selector.open();
步驟2:向Selector註冊通道
為了將Channel和Selector配合使用,實現單個線程處理多個通道的夢想,必須將channel註冊到selector上。可以通過SelectableChannel.register()方法來實現,如下:
channel.configureBlocking(false); SelectionKey key= channel.register(selector, Selectionkey.OP_READ);
其中第一句代碼設定通道為非阻塞模式,然後第二句向selector註冊該通道。register()方法的第二個參數。這是一個“interest集合”,意思是在通過Selector監聽Channel時對什麼事件感興趣。可以監聽四種不同類型的事件:
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
特別注意:與Selector一起使用時,Channel必須處於非阻塞模式下。這意味著不能將FileChannel與Selector一起使用,因為FileChannel不能切換到非阻塞模式。而通訊端通道都可以。
步驟3:阻塞監視通道
一旦向Selector註冊了一或多個通道,就可以調用幾個重載的select()方法。這些方法返回你所感興趣的事件(如串連、接受、讀或寫)已經準備就緒的那些通道。換句話說,如果你對“讀就緒”的通道感興趣,select()方法會返回讀事件已經就緒的那些通道。
下面是select()方法:
int select()int select(long timeout)int selectNow()
select()阻塞到至少有一個通道在你註冊的事件上就緒了。
select(long timeout)和select()一樣,除了最長會阻塞timeout毫秒(參數)。
selectNow()不會阻塞,不管什麼通道就緒都立刻返回(譯者註:此方法執行非阻塞的選擇操作。如果自從前一次選擇操作後,沒有通道變成可選擇的,則此方法直接返回零。)。
步驟4:遍曆selectedKeys()訪問就緒通道
一旦調用了select()方法,並且傳回值表明有一個或更多個通道就緒了,然後可以通過調用selector的selectedKeys()方法,訪問“已選擇鍵集(selected key set)”中的就緒通道。如下所示:
Set selectedKeys =selector.selectedKeys();
可以遍曆這個已選擇的鍵集合來訪問就緒的通道。
下面給出一個完整的Channel和Selector結合的例子:
Selector selector = Selector.open(); channel.configureBlocking(false); SelectionKey key =channel.register(selector, SelectionKey.OP_READ); while(true) { intreadyChannels = selector.select(); if(readyChannels == 0) continue; SetselectedKeys = 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 } } }
選取器+通訊端通道:
通訊端通道包括:SocketChannel、ServerSocketChannel和DatagramChannel。
SocketChannel是一個串連到TCP網路通訊端的通道。可以通過2種方式建立:
1) 開啟一個SocketChannel並串連到互連網上的某台伺服器。
2) 一個新串連到達ServerSocketChannel時建立一個SocketChannel。
ServerSocketChannel通過ServerSocketChannel.accept() 方法監聽新進來的串連。當 accept()方法返回的時候,它返回一個包含新進來的串連的 SocketChannel。因此,accept()方法會一直阻塞到有新串連到達。DatagramChannel是一個能收發UDP包的通道。因為UDP是不需連線的網路通訊協定,所以不能像其它通道那樣讀取和寫入。它發送和接收的是資料包。
將通訊端通道和Selector結合,因為選取器的多工特性(事件驅動)和通訊端通道的非阻塞特性,可有效地解決高並發環境下對於用戶端請求處理會耗費大量線程資源的情況。
(1)傳統的同步阻塞式IO(網路通訊端編程Socket):針對用戶端的每一個請求的串連,都需要分配一個單獨的線程進行處理,因為要隨時監視其是否有資料讀寫,而且在資料讀寫操作時,因為是阻塞式的,所以即使沒有資料到來,也會一直阻塞等待。這顯然會浪費大量CPU和線程資源。
(2)而多工選取器和非阻塞式的通訊端通道的結合:不但可以用一個線程來監視多個與用戶端建立的網路連接,還能夠在讀寫資料時,一旦資料沒有準備好,就立刻返回而不會阻塞(雖然實際上一旦執行讀寫是一般資料都已經準備好)。所以在並發較高的情境下,這種方式大大節約了CPU和線程(記憶體)資源。具體可以參考我這篇文章。
參考資料:
1 http://www.iteye.com/magazines/132-Java-NIO
2 http://www.cnblogs.com/dolphin0520/p/3919162.html
3 http://tutorials.jenkov.com/java-nio/nio-vs-io.html
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
Java基礎:非阻塞式IO