Java NIO使用及原理分析
http://blog.csdn.net/wuxianglong/article/details/6604817
轉載自:李會軍?寧靜致遠
最近由于工作關系要做一些Java方面的開發,其中最重要的一塊就是Java NIO(New I/O),盡管很早以前了解過一些,但并沒有認真去看過它的實現原理,也沒有機會在工作中使用,這次也好重新研究一下,順便寫點東西,就當是自己學習 Java NIO的筆記了。本文為NIO使用及原理分析的第一篇,將會介紹NIO中幾個重要的概念。
在Java1.4之前的I/O系統中,提供的都是面向流的I/O系統,系統一次一個字節地處理數據,一個輸入流產生一個字節的數據,一個輸出流消費一個字節的數據,面向流的I/O速度非常慢,而在Java 1.4中推出了NIO,這是一個面向塊的I/O系統,系統以塊的方式處理處理,每一個操作在一步中產生或者消費一個數據庫,按塊處理要比按字節處理數據快的多。
在NIO中有幾個核心對象需要掌握:緩沖區(Buffer)、通道(Channel)、選擇器(Selector)。
緩沖區Buffer
緩沖區實際上是一個容器對象,更直接的說,其實就是一個數組,在NIO庫中,所有數據都是用緩沖區處理的。在讀取數據時,它是直接讀到緩沖區中的; 在寫入數據時,它也是寫入到緩沖區中的;任何時候訪問 NIO 中的數據,都是將它放到緩沖區中。而在面向流I/O系統中,所有數據都是直接寫入或者直接將數據讀取到Stream對象中。
在NIO中,所有的緩沖區類型都繼承于抽象類Buffer,最常用的就是ByteBuffer,對于Java中的基本類型,基本都有一個具體Buffer類型與之相對應,它們之間的繼承關系如下圖所示:
下面是一個簡單的使用IntBuffer的例子:
[java] view plaincopyprint??
import java.nio.IntBuffer;public class TestIntBuffer {public static void main(String[] args) {// 分配新的int緩沖區,參數為緩沖區容量// 新緩沖區的當前位置將為零,其界限(限制位置)將為其容量。它將具有一個底層實現數組,其數組偏移量將為零。IntBuffer buffer = IntBuffer.allocate(8);for (int i = 0; i < buffer.capacity(); ++i) {int j = 2 * (i + 1);// 將給定整數寫入此緩沖區的當前位置,當前位置遞增buffer.put(j);}// 重設此緩沖區,將限制設置為當前位置,然后將當前位置設置為0buffer.flip();// 查看在當前位置和限制位置之間是否有元素while (buffer.hasRemaining()) {// 讀取此緩沖區當前位置的整數,然后當前位置遞增int j = buffer.get();System.out.print(j + " ");}}}運行后可以看到:
在后面我們還會繼續分析Buffer對象,以及它的幾個重要的屬性。
通道Channel
通道是一個對象,通過它可以讀取和寫入數據,當然了所有數據都通過Buffer對象來處理。我們永遠不會將字節直接寫入通道中,相反是將數據寫入包含一個或者多個字節的緩沖區。同樣不會直接從通道中讀取字節,而是將數據從通道讀入緩沖區,再從緩沖區獲取這個字節。
在NIO中,提供了多種通道對象,而所有的通道對象都實現了Channel接口。它們之間的繼承關系如下圖所示:
使用NIO讀取數據
在前面我們說過,任何時候讀取數據,都不是直接從通道讀取,而是從通道讀取到緩沖區。所以使用NIO讀取數據可以分為下面三個步驟:
1. 從FileInputStream獲取Channel
2. 創建Buffer
3. 將數據從Channel讀取到Buffer中
下面是一個簡單的使用NIO從文件中讀取數據的例子:
[java] view plaincopyprint??
import java.io.*; import java.nio.*; import java.nio.channels.*;public class Program {static public void main( String args[] ) throws Exception {FileInputStream fin = new FileInputStream("c:\\test.txt");// 獲取通道FileChannel fc = fin.getChannel();// 創建緩沖區ByteBuffer buffer = ByteBuffer.allocate(1024);// 讀取數據到緩沖區fc.read(buffer);buffer.flip();while (buffer.remaining()>0) {byte b = buffer.get();System.out.print(((char)b));}fin.close();} }使用NIO寫入數據
使用NIO寫入數據與讀取數據的過程類似,同樣數據不是直接寫入通道,而是寫入緩沖區,可以分為下面三個步驟:
1. 從FileInputStream獲取Channel
2. 創建Buffer
3. 將數據從Channel寫入到Buffer中
下面是一個簡單的使用NIO向文件中寫入數據的例子:
[java] view plaincopyprint??
import java.io.*; import java.nio.*; import java.nio.channels.*;public class Program {static private final byte message[] = { 83, 111, 109, 101, 32,98, 121, 116, 101, 115, 46 };static public void main( String args[] ) throws Exception {FileOutputStream fout = new FileOutputStream( "c:\\test.txt" );FileChannel fc = fout.getChannel();ByteBuffer buffer = ByteBuffer.allocate( 1024 );for (int i=0; i<message.length; ++i) {buffer.put( message[i] );}buffer.flip();fc.write( buffer );fout.close();} }本文介紹了Java NIO中三個核心概念中的兩個,并且看了兩個簡單的示例,分別是使用NIO進行數據的讀取和寫入,Java NIO中最重要的一塊Nonblocking I/O將在第三篇中進行分析,下篇將會介紹Buffer內部實現。
?
?
在第一篇中,我們介紹了NIO中的兩個核心對象:緩沖區和通道,在談到緩沖區時,我們說緩沖區對象本質上是一個數組,但它其實是一個特殊的數組,緩沖區對象內置了一些機制,能夠跟蹤和記錄緩沖區的狀態變化情況,如果我們使用get()方法從緩沖區獲取數據或者使用put()方法把數據寫入緩沖區,都會引起緩沖區狀態的變化。本文為NIO使用及原理分析的第二篇,將會分析NIO中的Buffer對象。
在緩沖區中,最重要的屬性有下面三個,它們一起合作完成對緩沖區內部狀態的變化跟蹤:
position:指定了下一個將要被寫入或者讀取的元素索引,它的值由get()/put()方法自動更新,在新創建一個Buffer對象時,position被初始化為0。
limit:指定還有多少數據需要取出(在從緩沖區寫入通道時),或者還有多少空間可以放入數據(在從通道讀入緩沖區時)。
capacity:指定了可以存儲在緩沖區中的最大數據容量,實際上,它指定了底層數組的大小,或者至少是指定了準許我們使用的底層數組的容量。
以上四個屬性值之間有一些相對大小的關系:0 <= position <= limit <= capacity。如果我們創建一個新的容量大小為10的ByteBuffer對象,在初始化的時候,position設置為0,limit和 capacity被設置為10,在以后使用ByteBuffer對象過程中,capacity的值不會再發生變化,而其它兩個個將會隨著使用而變化。四個屬性值分別如圖所示:
現在我們可以從通道中讀取一些數據到緩沖區中,注意從通道讀取數據,相當于往緩沖區中寫入數據。如果讀取4個自己的數據,則此時position的值為4,即下一個將要被寫入的字節索引為4,而limit仍然是10,如下圖所示:
下一步把讀取的數據寫入到輸出通道中,相當于從緩沖區中讀取數據,在此之前,必須調用flip()方法,該方法將會完成兩件事情:
1. 把limit設置為當前的position值
2. 把position設置為0
由于position被設置為0,所以可以保證在下一步輸出時讀取到的是緩沖區中的第一個字節,而limit被設置為當前的position,可以保證讀取的數據正好是之前寫入到緩沖區中的數據,如下圖所示:
現在調用get()方法從緩沖區中讀取數據寫入到輸出通道,這會導致position的增加而limit保持不變,但position不會超過limit的值,所以在讀取我們之前寫入到緩沖區中的4個自己之后,position和limit的值都為4,如下圖所示:
在從緩沖區中讀取數據完畢后,limit的值仍然保持在我們調用flip()方法時的值,調用clear()方法能夠把所有的狀態變化設置為初始化時的值,如下圖所示:
最后我們用一段代碼來驗證這個過程,如下所示:
[java] view plaincopyprint??
import java.io.*; import java.nio.*; import java.nio.channels.*;public class Program {public static void main(String args[]) throws Exception {FileInputStream fin = new FileInputStream("d:\\test.txt");FileChannel fc = fin.getChannel();ByteBuffer buffer = ByteBuffer.allocate(10);output("初始化", buffer);fc.read(buffer);output("調用read()", buffer);buffer.flip();output("調用flip()", buffer);while (buffer.remaining() > 0) {byte b = buffer.get();// System.out.print(((char)b));}output("調用get()", buffer);buffer.clear();output("調用clear()", buffer);fin.close();}public static void output(String step, Buffer buffer) {System.out.println(step + " : ");System.out.print("capacity: " + buffer.capacity() + ", ");System.out.print("position: " + buffer.position() + ", ");System.out.println("limit: " + buffer.limit());System.out.println();} }完成的輸出結果為:
這與我們上面演示的過程一致。在后面的文章中,我們繼續介紹NIO中關于緩沖區一些更高級的使用。
?
?
在上一篇文章中介紹了緩沖區內部對于狀態變化的跟蹤機制,而對于NIO中緩沖區來說,還有很多的內容值的學習,如緩沖區的分片與數據共享,只讀緩沖區等。在本文中我們來看一下緩沖區一些更細節的內容。
緩沖區的分配
在前面的幾個例子中,我們已經看過了,在創建一個緩沖區對象時,會調用靜態方法allocate()來指定緩沖區的容量,其實調用 allocate()相當于創建了一個指定大小的數組,并把它包裝為緩沖區對象。或者我們也可以直接將一個現有的數組,包裝為緩沖區對象,如下示例代碼所示:
[java] view plaincopyprint??
public class BufferWrap {public void myMethod(){// 分配指定大小的緩沖區ByteBuffer buffer1 = ByteBuffer.allocate(10);// 包裝一個現有的數組byte array[] = new byte[10];ByteBuffer buffer2 = ByteBuffer.wrap( array );} }緩沖區分片
在NIO中,除了可以分配或者包裝一個緩沖區對象外,還可以根據現有的緩沖區對象來創建一個子緩沖區,即在現有緩沖區上切出一片來作為一個新的緩沖區,但現有的緩沖區與創建的子緩沖區在底層數組層面上是數據共享的,也就是說,子緩沖區相當于是現有緩沖區的一個視圖窗口。調用slice()方法可以創建一個子緩沖區,讓我們通過例子來看一下:
[java] view plaincopyprint? import java.nio.*;public class Program {static public void main( String args[] ) throws Exception {ByteBuffer buffer = ByteBuffer.allocate( 10 );// 緩沖區中的數據0-9for (int i=0; i<buffer.capacity(); ++i) {buffer.put( (byte)i );}// 創建子緩沖區buffer.position( 3 );buffer.limit( 7 );ByteBuffer slice = buffer.slice();// 改變子緩沖區的內容for (int i=0; i<slice.capacity(); ++i) {byte b = slice.get( i );b *= 10;slice.put( i, b );}buffer.position( 0 );buffer.limit( buffer.capacity() );while (buffer.remaining()>0) {System.out.println( buffer.get() );}} }在該示例中,分配了一個容量大小為10的緩沖區,并在其中放入了數據0-9,而在該緩沖區基礎之上又創建了一個子緩沖區,并改變子緩沖區中的內容,從最后輸出的結果來看,只有子緩沖區“可見的”那部分數據發生了變化,并且說明子緩沖區與原緩沖區是數據共享的,輸出結果如下所示:
只讀緩沖區
只讀緩沖區非常簡單,可以讀取它們,但是不能向它們寫入數據。可以通過調用緩沖區的asReadOnlyBuffer()方法,將任何常規緩沖區轉 換為只讀緩沖區,這個方法返回一個與原緩沖區完全相同的緩沖區,并與原緩沖區共享數據,只不過它是只讀的。如果原緩沖區的內容發生了變化,只讀緩沖區的內容也隨之發生變化:
[java] view plaincopyprint? import java.nio.*;public class Program {static public void main( String args[] ) throws Exception {ByteBuffer buffer = ByteBuffer.allocate( 10 );// 緩沖區中的數據0-9for (int i=0; i<buffer.capacity(); ++i) {buffer.put( (byte)i );}// 創建只讀緩沖區ByteBuffer readonly = buffer.asReadOnlyBuffer();// 改變原緩沖區的內容for (int i=0; i<buffer.capacity(); ++i) {byte b = buffer.get( i );b *= 10;buffer.put( i, b );}readonly.position(0);readonly.limit(buffer.capacity());// 只讀緩沖區的內容也隨之改變while (readonly.remaining()>0) {System.out.println( readonly.get());}} }如果嘗試修改只讀緩沖區的內容,則會報ReadOnlyBufferException異常。只讀緩沖區對于保護數據很有用。在將緩沖區傳遞給某個 對象的方法時,無法知道這個方法是否會修改緩沖區中的數據。創建一個只讀的緩沖區可以保證該緩沖區不會被修改。只可以把常規緩沖區轉換為只讀緩沖區,而不能將只讀的緩沖區轉換為可寫的緩沖區。
直接緩沖區
直接緩沖區是為加快I/O速度,使用一種特殊方式為其分配內存的緩沖區,JDK文檔中的描述為:給定一個直接字節緩沖區,Java虛擬機將盡最大努 力直接對它執行本機I/O操作。也就是說,它會在每一次調用底層操作系統的本機I/O操作之前(或之后),嘗試避免將緩沖區的內容拷貝到一個中間緩沖區中 或者從一個中間緩沖區中拷貝數據。要分配直接緩沖區,需要調用allocateDirect()方法,而不是allocate()方法,使用方式與普通緩沖區并無區別,如下面的拷貝文件示例:
[java] view plaincopyprint? import java.io.*; import java.nio.*; import java.nio.channels.*;public class Program {static public void main( String args[] ) throws Exception {String infile = "c:\\test.txt";FileInputStream fin = new FileInputStream( infile );FileChannel fcin = fin.getChannel();String outfile = String.format("c:\\testcopy.txt");FileOutputStream fout = new FileOutputStream( outfile ); FileChannel fcout = fout.getChannel();// 使用allocateDirect,而不是allocateByteBuffer buffer = ByteBuffer.allocateDirect( 1024 );while (true) {buffer.clear();int r = fcin.read( buffer );if (r==-1) {break;}buffer.flip();fcout.write( buffer );}} }內存映射文件I/O
內存映射文件I/O是一種讀和寫文件數據的方法,它可以比常規的基于流或者基于通道的I/O快的多。內存映射文件I/O是通過使文件中的數據出現為 內存數組的內容來完成的,這其初聽起來似乎不過就是將整個文件讀到內存中,但是事實上并不是這樣。一般來說,只有文件中實際讀取或者寫入的部分才會映射到內存中。如下面的示例代碼:
[java] view plaincopyprint??
import java.io.*; import java.nio.*; import java.nio.channels.*;public class Program {static private final int start = 0;static private final int size = 1024;static public void main( String args[] ) throws Exception {RandomAccessFile raf = new RandomAccessFile( "c:\\test.txt", "rw" );FileChannel fc = raf.getChannel();MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE,start, size );mbb.put( 0, (byte)97 );mbb.put( 1023, (byte)122 );raf.close();} }關于緩沖區的細節內容,我們已經用了兩篇文章來介紹。在下一篇中將會介紹NIO中更有趣的部分Nonblocking I/O。
?
在上一篇文章中介紹了關于緩沖區的一些細節內容,現在終于可以進入NIO中最有意思的部分非阻塞I/O。通常在進行同步I/O操作時,如果讀取數據,代碼會阻塞直至有 可供讀取的數據。同樣,寫入調用將會阻塞直至數據能夠寫入。傳統的Server/Client模式會基于TPR(Thread per Request),服務器會為每個客戶端請求建立一個線程,由該線程單獨負責處理一個客戶請求。這種模式帶來的一個問題就是線程數量的劇增,大量的線程會增大服務器的開銷。大多數的實現為了避免這個問題,都采用了線程池模型,并設置線程池線程的最大數量,這由帶來了新的問題,如果線程池中有200個線程,而有200個用戶都在進行大文件下載,會導致第201個用戶的請求無法及時處理,即便第201個用戶只想請求一個幾KB大小的頁面。傳統的 Server/Client模式如下圖所示:
NIO中非阻塞I/O采用了基于Reactor模式的工作方式,I/O調用不會被阻塞,相反是注冊感興趣的特定I/O事件,如可讀數據到達,新的套接字連接等等,在發生特定事件時,系統再通知我們。NIO中實現非阻塞I/O的核心對象就是Selector,Selector就是注冊各種I/O事件地 方,而且當那些事件發生時,就是這個對象告訴我們所發生的事件,如下圖所示:
從圖中可以看出,當有讀或寫等任何注冊的事件發生時,可以從Selector中獲得相應的SelectionKey,同時從 SelectionKey中可以找到發生的事件和該事件所發生的具體的SelectableChannel,以獲得客戶端發送過來的數據。關于 SelectableChannel的可以參考Java NIO使用及原理分析(一)
使用NIO中非阻塞I/O編寫服務器處理程序,大體上可以分為下面三個步驟:
1. 向Selector對象注冊感興趣的事件
2. 從Selector中獲取感興趣的事件
3. 根據不同的事件進行相應的處理
接下來我們用一個簡單的示例來說明整個過程。首先是向Selector對象注冊感興趣的事件:
[java] view plaincopyprint??
/** 注冊事件* */ protected Selector getSelector() throws IOException {// 創建Selector對象Selector sel = Selector.open();// 創建可選擇通道,并配置為非阻塞模式ServerSocketChannel server = ServerSocketChannel.open();server.configureBlocking(false);// 綁定通道到指定端口ServerSocket socket = server.socket();InetSocketAddress address = new InetSocketAddress(port);socket.bind(address);// 向Selector中注冊感興趣的事件server.register(sel, SelectionKey.OP_ACCEPT); return sel; }創建了ServerSocketChannel對象,并調用configureBlocking()方法,配置為非阻塞模式,接下來的三行代碼把該通道綁定到指定端口,最后向Selector中注冊事件,此處指定的是參數是OP_ACCEPT,即指定我們想要監聽accept事件,也就是新的連接發 生時所產生的事件,對于ServerSocketChannel通道來說,我們唯一可以指定的參數就是OP_ACCEPT。
從Selector中獲取感興趣的事件,即開始監聽,進入內部循環:
[java] view plaincopyprint? * 開始監聽* */ public void listen() { System.out.println("listen on " + port);try { while(true) { // 該調用會阻塞,直到至少有一個事件發生selector.select(); Set<SelectionKey> keys = selector.selectedKeys();Iterator<SelectionKey> iter = keys.iterator();while (iter.hasNext()) { SelectionKey key = (SelectionKey) iter.next(); iter.remove(); process(key); } } } catch (IOException e) { e.printStackTrace();} }在非阻塞I/O中,內部循環模式基本都是遵循這種方式。首先調用select()方法,該方法會阻塞,直到至少有一個事件發生,然后再使用selectedKeys()方法獲取發生事件的SelectionKey,再使用迭代器進行循環。
最后一步就是根據不同的事件,編寫相應的處理代碼:
[java] view plaincopyprint??
/** 根據不同的事件做處理* */ protected void process(SelectionKey key) throws IOException{// 接收請求if (key.isAcceptable()) {ServerSocketChannel server = (ServerSocketChannel) key.channel();SocketChannel channel = server.accept();channel.configureBlocking(false);channel.register(selector, SelectionKey.OP_READ);}// 讀信息else if (key.isReadable()) {SocketChannel channel = (SocketChannel) key.channel(); int count = channel.read(buffer); if (count > 0) { buffer.flip(); CharBuffer charBuffer = decoder.decode(buffer); name = charBuffer.toString(); SelectionKey sKey = channel.register(selector, SelectionKey.OP_WRITE); sKey.attach(name); } else { channel.close(); } buffer.clear(); }// 寫事件else if (key.isWritable()) {SocketChannel channel = (SocketChannel) key.channel(); String name = (String) key.attachment(); ByteBuffer block = encoder.encode(CharBuffer.wrap("Hello " + name)); if(block != null){channel.write(block);}else{channel.close();}} }此處分別判斷是接受請求、讀數據還是寫事件,分別作不同的處理。
到這里關于Java NIO使用及原理分析的四篇文章就全部完成了。Java NIO提供了通道、緩沖區、選擇器這樣一組抽象概念,極大的簡化了我們編寫高性能并發型服務器程序,后面有機會我會繼續談談使用Java NIO的一些想法。
?
?
總結
以上是生活随笔為你收集整理的Java NIO使用及原理分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ORA-01855: AM/A.M. o
- 下一篇: java BIO NIO AIO 理论篇