Java NIO
一、Java I/O系統
Java I/O系統從流結構上可分為字節流(以字節為處理單位或稱面向字節)和字符流(以字符為處理單位或稱面向字符)。
字節流的輸人流和輸出流基礎是InputStream和OutputStream這兩個抽象類,字節流的輸人輸出操作由這兩個類的子類實現。字符流是Java1.1版后新增加的以字符為單位進行輸人輸出處理的流,字符流輸入輸出的基礎是抽象類Reader和Writer。但是最底層都是字節流,字符流的出現有助于我們對文件的讀取,例如按行讀取之類的。
流又分為節點流和過濾流:
節點流:從特定的地方讀寫的流類,例如:磁盤或一塊內存區域或者鍵盤的輸入。
過濾流:使用節點流作為輸人或輸出。過濾流是使用一個已經存在的輸入流或輸出流(可以是節點流也可以是過濾流)連接創建的。
如下圖所示:
以數據寫入讀取為例,我們可以以以下方式構建流的鏈:
以下是字符流的輸入輸出的繼承關系:
以上就是傳統的JAVA I/O系統。
二、初步了解Java NIO
java io的核心概念是流,面向流的編程。
java nio中有3個核心概念:Selector、Channel、Buffer,面向塊編程。
他們的關系如下圖所示,每個Channel里又有Buffer。Selector可以選擇連接到哪個Channel,從而從中讀取寫入數據。我們的數據交換通過Buffer來進行,所以可以使用Buffer來讀也可以寫。所有數據都是通過Buffer來進行的。我們也可以單獨使用Channle和Buffer而不使用Selector,如下面的文件讀取示例,關于Selector和Channle還有Buffer的結合使用,在講Selector的時候會給出例子。流是單向的,而Channel是雙向的。此外,Java中的8中原生數據類型除了boolean都有對應的Buffer類型,IntBuffer、LongBuffer等等。
下面是使用NIO的一個例子:
三、Java NIO中的Buffer詳解
Buffer有三個重要的狀態屬性:position、limit、capacity;
位置(position):當前緩沖區(Buffer)的位置,將從該位置往后讀或寫數據。
容量(capacity):緩沖區的總容量上限(不會改變)。
上限(limit):緩沖區的實際容量大小。
我們結合下圖來理解:
首先,在寫模式中。
position最開始指向0的位置,limit和capacity都指向最后一個位置,然后每寫一個position就向后移動一個。limit就像字面意思一樣,最多寫到limit指向的位置。
我們調用 byteBuffer.flip();轉換到讀模式(也就是 limit = position; position = 0;)
寫模式中各個位置如上圖所示,position指向0的位置,limit指向之前寫到的最大的位置,capacity不變。每讀一個position就向后移動一個,limit是最多能讀到的位置。
如果查看NIO的Buffer API文檔,你會發現Buffer的很多操作都是圍繞這三個值來進行的。比如上面的NIO例子代碼中有:byteBuffer.remaining(),它就是返回當前位置和限制之間的元素數, 即limit減position的值;byteBuffer.get()會增加一個position的值。
更多內容可以查看JAVA API文檔。查看Buffer的源代碼也是一個不錯的選擇,例如你可以查看到在Buffer中的flip方法:
public final Buffer flip() {limit = position;position = 0;mark = -1;return this;}代碼中很直白的告訴里你將會對上面所說的值做哪些操作。
現在我們關注一下Buffer申請內存的代碼。即:ByteBuffer.allocate(10)。實際上分析它還是比較容易的,我們直接在IDEA中按住Ctrl點擊這方法來到實現:
public static ByteBuffer allocate(int capacity) {if (capacity < 0)throw new IllegalArgumentException();return new HeapByteBuffer(capacity, capacity); }再點擊HeapByteBuffer
HeapByteBuffer(int cap, int lim) { // package-privatesuper(-1, 0, lim, cap, new byte[cap], 0);/*hb = new byte[cap];offset = 0;*/ }點擊super,注意這里的new byte[cap]參數。
ByteBuffer(int mark, int pos, int lim, int cap, // package-privatebyte[] hb, int offset) {super(mark, pos, lim, cap);this.hb = hb;this.offset = offset; }最后可以看到,this.hb = hb,結合new byte[cap]參數,也就是直接在堆上申請一塊內存,作為Buffer對象具體存放數據的空間。實際上也比較符合HeapByteBuffer這個名字。在ByteBuffer中還有一個申請內存來存放數據的方法:allocateDirect;要理解這個方法,就必須對JVM的內存模型有一些了解,我們知道JVM中有一個堆外內存的概念。ByteBuffer中使用allocateDirect申請內存就是在堆外內存中申請的。在使用allocateDirect申請的Buffer對象中有一個address變量指向這個堆外內存的地址。
為什么不直接使用堆上的內存呢?實際上是出于對效率的考慮,在Java堆上的內存,操作系統不會直接使用(注意是不會而不是不能),而是在操作系統內存中又申請一塊空間。JAVA堆上的數據還要拷貝到操作系統申請的空間上,操作系統再和IO設備進行交互,而使用allocateDirect,將省略這樣一次拷貝,也就是0拷貝的概念。
這里做一個擴展,為什么操作系統不直接使用JAVA堆上的數據來和IO設備交互呢?這里就需要對GC有一個理解了,GC的過程中會改變對象的內存位置。而在和IO設備交互的時候顯然這個地址不能改變,JVM就必須保證不會進行GC,和IO設備交互是比較耗費時間的,而長時間不進行GC的話必然導致堆的溢出,所以我們可以先拷貝到操作系統的內存中,而后這塊內存再和IO設備交互。因為拷貝比直接和IO設備交互要快很多,而短時間的停止GC是可以接受的。
下面說說一個常用的概念。
- 內存映射文件
內存映射文件能讓你創建和修改那些因為太大而無法放入內存的文件。有了內存映射文件,你就可以認為文件已經全部讀進了內存,然后把它當成一個非常大的數組來訪問。這種解決辦法能大大簡化修改文件的代碼。直接修改內存數據,就會修改文件數據,而不用像通常一樣需要文件讀寫,加快了文件訪問的速度。內存映射文件的實現是操作系統來維護的。在Nio中可以使用ByteBuffer的子類MappedByteBuffer來進行相應的操作,具體做法可以參考:https://blog.csdn.net/akon_vm/article/details/7429245
四、Java NIO中的Scattering 和 Gathering (散開和聚合)
之前我們的Channel只有一個Buffer,但是實際上我們可以將一個Channel中的數據寫到多個Buffer中(Scattering散開),或者將多個Buffer中的數據寫到一個Channel中(Gathering聚合)。
在從Channel中讀數據寫到多個Buffer中的時候,先寫滿第一個Buffer。多個Buffer中的數據寫到Channel中的時候,也是先寫完第一個Buffer。具體的操作可以查看下面的實例代碼。
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Arrays; /*** Scattering:在將數據寫入到buffer中時,可以采用buffer數組,依次寫入,一個buffer滿了就寫下一個。* Gatering:在將數據讀出到buffer中時,可以采用buffer數組,依次讀入,一個buffer滿了就讀下一個。*//*** 使用方式:打開cmd telnet locakhost 8899* 連接后輸入字符串,在控制臺會輸出每個Buffer信息。*/ public class NioTestServer {public static void main(String[] args) throws IOException {ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();InetSocketAddress address=new InetSocketAddress(8899);serverSocketChannel.socket().bind(address);int messageLength=2+3+5;ByteBuffer[] byteBuffers=new ByteBuffer[3];byteBuffers[0]=ByteBuffer.allocate(2);byteBuffers[1]=ByteBuffer.allocate(3);byteBuffers[2]=ByteBuffer.allocate(5);SocketChannel socketChannel=serverSocketChannel.accept();while (true){int byteRead=0;//接受客戶端寫入的的字符串while(byteRead<messageLength){long r=socketChannel.read(byteBuffers);byteRead+=r;System.out.println("byteRead:"+byteRead);//通過流打印每個Buffer的position和limit信息Arrays.asList(byteBuffers).stream().map(buffer -> "position:"+ buffer.position() +",limit:"+buffer.limit()).forEach(System.out::println);}//將所有buffer都flip。Arrays.asList(byteBuffers).forEach(buffer -> {buffer.flip();});//將數據讀出回顯到客戶端long byteWrite=0;while (byteWrite < messageLength) {long r=socketChannel.write(byteBuffers);byteWrite+=r;}//將所有buffer都clearArrays.asList(byteBuffers).forEach(buffer -> {buffer.clear();});System.out.println("byteRead:"+byteRead+",byteWrite:"+byteWrite+",messageLength:"+messageLength);}} }五、Java NIO中的Selector
在普通的server操作中:
..... while(ture){Socket socket = serverSocket.accept();//阻塞直到有客戶端連接過來socket.getInputSteam();............... } ..... ..... .....如果有多個客戶端,這樣顯然是不合適的,因為無法即時響應客戶端。改進如下:
..... while(true){Socket socket = serverSocket.accept();//阻塞直到有客戶端連接過來new Thread(socket); } ..... ..... .....但是對于多個客戶端連接的話,服務器肯定會起多個線程,這對服務器來說壓力肯定是非常大的,而且頻繁的線程間切換也會損耗性能,NIO的出現解決了這個問題,一個線程可以處理多個客戶端事件,具體的工作我們可以再交給具體的任務線程。Selector的具體操作可以查看這里;下面是一個例子:
import java.io.IOException; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.Set;public class NioTest4Server {public static void main(String[] args) throws IOException {int [] ports=new int[5];ports[0]=5000;ports[1]=5001;ports[2]=5002;ports[3]=5003;ports[4]=5004;//一般創建selector的方法Selector selector=Selector.open();//for循環用來將多個端口地址和通道綁定for (int i=0;i<5;i++){//打開ServerSocketChannel通道ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();//通道必須配置成非阻塞狀態,FileChannel是無法配置成非阻塞狀態的,所以它不能使用下面的訪問方式。serverSocketChannel.configureBlocking(false);//通過ServerSocketChannel的socket()方法獲得serverSocket對象。ServerSocket serverSocket=serverSocketChannel.socket();//將每一個serverSocket和端口號綁定InetSocketAddress address=new InetSocketAddress(ports[i]);serverSocket.bind(address);// 將channel注冊到selector上,只對感興趣的事件監聽serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);/*** 通道觸發了一個事件意思是該事件已經就緒。四種事件,用四種key表示。* SelectionKey.OP_CONNECT:某個channel成功連接到另一個服務器稱為“連接就緒”。* SelectionKey.OP_ACCEPT:一個server socket channel準備好接收新進入的連接稱為“接收就緒”。* SelectionKey.OP_READ:一個有數據可讀的通道可以說是“讀就緒”。* SelectionKey.OP_WRITE:等待寫數據的通道可以說是“寫就緒”。*/System.out.println("監聽端口為:"+ports[i]);}/*** 一個Selector對象會包含3種類型的SelectionKey集合:** all-keys:當前所有向Selector注冊的SelectionKey的集合,Selector的keys()方法返回該集合** 當register()方法執行時,新建一個SelectioKey,并把它加入Selector的all-keys集合中。** selected-keys:相關事件已經被Selector捕獲的SelectionKey的集合,Selector的selectedKeys()方法返回該集合** 在執行Selector的select()方法時,如果與SelectionKey相關的事件發生了,* 這個SelectionKey就被加入到selected-keys集合中,程序直接調用selected-keys集合的remove()方法,* 或者調用它的iterator的remove()方法,都可以從selected-keys集合中刪除一個SelectionKey對象。** cancelled-keys:已經被取消的SelectionKey的集合,Selector沒有提供訪問這種集合的方法** 如果關閉了與SelectionKey對象關聯的Channel對象,或者調用了SelectionKey對象的cancel方法,* 這個SelectionKey對象就會被加入到cancelled-keys集合中,表示這個SelectionKey對象已經被取消。*/while(true){//阻塞,直到有事件發送int keyNumbers=selector.select();System.out.println("返回key的數量:"+ keyNumbers);//獲得所有SelectionKey,因為同一時間可能連接多個channel,從而產生多個SelectionKeySet<SelectionKey> selectionKeys=selector.selectedKeys();//迭代所有已經獲得的SelectedkeyIterator<SelectionKey> iterator= selectionKeys.iterator();//迭代selectionKeyswhile (iterator.hasNext()){SelectionKey selectionKey=iterator.next();if (selectionKey.isAcceptable()){//通過key來獲得發送事件的通道ServerSocketChannel serverSocketChannel= (ServerSocketChannel) selectionKey.channel();//如果客戶端連接,獲得客戶端channelSocketChannel socketChannel=serverSocketChannel.accept();socketChannel.configureBlocking(false);//通過selector來監聽讀事件socketChannel.register(selector, SelectionKey.OP_READ);//如果不移除這個key,他還存在在Selectedkey集合中,那么下次迭代他還存在iterator.remove();System.out.println("獲取客戶端連接:"+socketChannel);} else if (selectionKey.isReadable()){SocketChannel socketChannel= (SocketChannel) selectionKey.channel();ByteBuffer byteBuffer=ByteBuffer.allocate(512);while(true){byteBuffer.clear();int read= socketChannel.read(byteBuffer);if (read<=0){break;}byteBuffer.flip();socketChannel.write(byteBuffer);}Charset charset = Charset.forName("utf-8");String massage = String.valueOf(charset.decode(byteBuffer).array());System.out.println("讀取:"+massage+",來自于"+socketChannel);iterator.remove();}}}} }以上就是JAVA NIO系統主要內容的講解了,Selector是NIO中最重要的內容,一定要好好理解。
總結
- 上一篇: 使用Spring-AOP
- 下一篇: 操作系统中的零拷贝与java中的使用