java IO、NIO、AIO詳解

 

正文html

回到頂部java

概述

在咱們學習Java的IO流以前,咱們都要了解幾個關鍵詞編程

  • 同步與異步(synchronous/asynchronous):同步是一種可靠的有序運行機制,當咱們進行同步操做時,後續的任務是等待當前調用返回,纔會進行下一步;而異步則相反,其餘任務不須要等待當前調用返回,一般依靠事件、回調等機制來實現任務間次序關係
  • 阻塞與非阻塞:在進行阻塞操做時,當前線程會處於阻塞狀態,沒法從事其餘任務,只有當條件就緒才能繼續,好比ServerSocket新鏈接創建完畢,或者數據讀取、寫入操做完成;而非阻塞則是無論IO操做是否結束,直接返回,相應操做在後臺繼續處理

同步和異步的概念:實際的I/O操做數組

同步是用戶線程發起I/O請求後須要等待或者輪詢內核I/O操做完成後才能繼續執行網絡

異步是用戶線程發起I/O請求後仍須要繼續執行,當內核I/O操做完成後會通知用戶線程,或者調用用戶線程註冊的回調函數多線程

阻塞和非阻塞的概念:發起I/O請求併發

阻塞是指I/O操做須要完全完成後才能返回用戶空間框架

非阻塞是指I/O操做被調用後當即返回一個狀態值,無需等I/O操做完全完成異步

BIO、NIO、AIO的概述

首先,傳統的 java.io包,它基於流模型實現,提供了咱們最熟知的一些 IO 功能,好比 File 抽象、輸入輸出流等。交互方式是同步、阻塞的方式,也就是說,在讀取輸入流或者寫入輸出流時,在讀、寫動做完成以前,線程會一直阻塞在那裏,它們之間的調用是可靠的線性順序。socket

java.io包的好處是代碼比較簡單、直觀,缺點則是 IO 效率和擴展性存在侷限性,容易成爲應用性能的瓶頸。

不少時候,人們也把 java.net下面提供的部分網絡 API,好比 Socket、ServerSocket、HttpURLConnection 也歸類到同步阻塞 IO 類庫,由於網絡通訊一樣是 IO 行爲。

第二,在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,能夠構建多路複用的、同步非阻塞 IO 程序,同時提供了更接近操做系統底層的高性能數據操做方式。

第三,在 Java 7 中,NIO 有了進一步的改進,也就是 NIO 2,引入了異步非阻塞 IO 方式,也有不少人叫它 AIO(Asynchronous IO)。異步 IO 操做基於事件和回調機制,能夠簡單理解爲,應用操做直接返回,而不會阻塞在那裏,當後臺處理完成,操做系統會通知相應線程進行後續工做。

回到頂部

1、IO流(同步、阻塞)

一、概述

IO流簡單來講就是input和output流,IO流主要是用來處理設備之間的數據傳輸,Java IO對於數據的操做都是經過流實現的,而java用於操做流的對象都在IO包中。

二、分類

按操做數據分爲:字節流(Reader、Writer)和字符流(InputStream、OutputStream)

按流向分:輸入流(Reader、InputStream)和輸出流(Writer、OutputStream)

三、字符流

概述

只用來處理文本數據

數據最多見的表現形式是文件,字符流用來操做文件的子類通常是FileReader和FileWriter

字符流讀寫文件注意事項:

  • 寫入文件必需要用flush()刷新
  • 用完流記得要關閉流
  • 使用流對象要拋出IO異常
  • 定義文件路徑時,能夠用"/"或者"\"
  • 在建立一個文件時,若是目錄下有同名文件將被覆蓋
  • 在讀取文件時,必須保證該文件已存在,不然拋出異常

字符流的緩衝區

  • 緩衝區的出現是爲了提升流的操做效率而出現的
  • 須要被提升效率的流做爲參數傳遞給緩衝區的構造函數
  • 在緩衝區中封裝了一個數組,存入數據後一次取出

四、字節流

概述

用來處理媒體數據

字節流讀寫文件注意事項:

  • 字節流和字符流的基本操做是相同的,可是想要操做媒體流就須要用到字節流
  • 字節流由於操做的是字節,因此能夠用來操做媒體文件(媒體文件也是以字節存儲的)
  • 輸入流(InputStream)、輸出流(OutputStream)
  • 字節流操做能夠不用刷新流操做
  • InputStream特有方法:int available()(返回文件中的字節個數)

 

字節流的緩衝區
字節流緩衝區跟字符流緩衝區同樣,也是爲了提升效率

五、Java Scanner類

Java 5添加了java.util.Scanner類,這是一個用於掃描輸入文本的新的實用程序

關於nextInt()、next()、nextLine()的理解

nextInt():只能讀取數值,如果格式不對,會拋出java.util.InputMismatchException異常

next():碰見第一個有效字符(非空格,非換行符)時,開始掃描,當碰見第一個分隔符或結束符(空格或換行符)時,結束掃描,獲取掃描到的內容

nextLine():能夠掃描到一行內容並做爲字符串而被捕獲到

關於hasNext()、hasNextLine()、hasNextxxx()的理解

就是爲了判斷輸入行中是否還存在xxx的意思

與delimiter()有關的方法

應該是輸入內容的分隔符設置,

回到頂部

2、NIO(同步、非阻塞)

NIO之因此是同步,是由於它的accept/read/write方法的內核I/O操做都會阻塞當前線程

首先,咱們要先了解一下NIO的三個主要組成部分:Channel(通道)、Buffer(緩衝區)、Selector(選擇器)

(1)Channel(通道)

Channel(通道):Channel是一個對象,能夠經過它讀取和寫入數據。能夠把它看作是IO中的流,不一樣的是:

  • Channel是雙向的,既能夠讀又能夠寫,而流是單向的
  • Channel能夠進行異步的讀寫
  • 對Channel的讀寫必須經過buffer對象

正如上面提到的,全部數據都經過Buffer對象處理,因此,您永遠不會將字節直接寫入到Channel中,相反,您是將數據寫入到Buffer中;一樣,您也不會從Channel中讀取字節,而是將數據從Channel讀入Buffer,再從Buffer獲取這個字節。

由於Channel是雙向的,因此Channel能夠比流更好地反映出底層操做系統的真實狀況。特別是在Unix模型中,底層操做系統一般都是雙向的。

在Java NIO中的Channel主要有以下幾種類型:

  • FileChannel:從文件讀取數據的
  • DatagramChannel:讀寫UDP網絡協議數據
  • SocketChannel:讀寫TCP網絡協議數據
  • ServerSocketChannel:能夠監聽TCP鏈接

(2)Buffer

Buffer是一個對象,它包含一些要寫入或者讀到Stream對象的。應用程序不能直接對 Channel 進行讀寫操做,而必須經過 Buffer 來進行,即 Channel 是經過 Buffer 來讀寫數據的。

在NIO中,全部的數據都是用Buffer處理的,它是NIO讀寫數據的中轉池。Buffer實質上是一個數組,一般是一個字節數據,但也能夠是其餘類型的數組。但一個緩衝區不只僅是一個數組,重要的是它提供了對數據的結構化訪問,並且還能夠跟蹤系統的讀寫進程。

使用 Buffer 讀寫數據通常遵循如下四個步驟:

1.寫入數據到 Buffer;

2.調用 flip() 方法;

3.從 Buffer 中讀取數據;

4.調用 clear() 方法或者 compact() 方法。

當向 Buffer 寫入數據時,Buffer 會記錄下寫了多少數據。一旦要讀取數據,須要經過 flip() 方法將 Buffer 從寫模式切換到讀模式。在讀模式下,能夠讀取以前寫入到 Buffer 的全部數據。

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

Buffer主要有以下幾種:

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

copyFile實例(NIO)

CopyFile是一個很是好的讀寫結合的例子,咱們將經過CopyFile這個實力讓你們體會NIO的操做過程。CopyFile執行三個基本的操做:建立一個Buffer,而後從源文件讀取數據到緩衝區,而後再將緩衝區寫入目標文件。

public static void copyFileUseNIO(String src,String dst) throws IOException{
//聲明源文件和目標文件
        FileInputStream fi=new FileInputStream(new File(src));
        FileOutputStream fo=new FileOutputStream(new File(dst));
        //得到傳輸通道channel
        FileChannel inChannel=fi.getChannel();
        FileChannel outChannel=fo.getChannel();
        //得到容器buffer
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        while(true){
            //判斷是否讀完文件
            int eof =inChannel.read(buffer);
            if(eof==-1){
                break;  
            }
            //重設一下buffer的position=0,limit=position
            buffer.flip();
            //開始寫
            outChannel.write(buffer);
            //寫完要重置buffer,重設position=0,limit=capacity
            buffer.clear();
        }
        inChannel.close();
        outChannel.close();
        fi.close();
        fo.close();
}

(三)Selector(選擇器對象)

首先須要瞭解一件事情就是線程上下文切換開銷會在高併發時變得很明顯,這是同步阻塞方式的低擴展性劣勢。

Selector是一個對象,它能夠註冊到不少個Channel上,監聽各個Channel上發生的事件,而且可以根據事件狀況決定Channel讀寫。這樣,經過一個線程管理多個Channel,就能夠處理大量網絡鏈接了。

selector優勢

有了Selector,咱們就能夠利用一個線程來處理全部的channels。線程之間的切換對操做系統來講代價是很高的,而且每一個線程也會佔用必定的系統資源。因此,對系統來講使用的線程越少越好。

1.如何建立一個Selector

Selector 就是您註冊對各類 I/O 事件興趣的地方,並且當那些事件發生時,就是這個對象告訴您所發生的事件。

Selector selector = Selector.open();

2.註冊Channel到Selector

爲了能讓Channel和Selector配合使用,咱們須要把Channel註冊到Selector上。經過調用 channel.register()方法來實現註冊:

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

注意,註冊的Channel 必須設置成異步模式 才能夠,不然異步IO就沒法工做,這就意味着咱們不能把一個FileChannel註冊到Selector,由於FileChannel沒有異步模式,可是網絡編程中的SocketChannel是能夠的。

3.關於SelectionKey

請注意對register()的調用的返回值是一個SelectionKey。 SelectionKey 表明這個通道在此 Selector 上註冊。當某個 Selector 通知您某個傳入事件時,它是經過提供對應於該事件的 SelectionKey 來進行的。SelectionKey 還能夠用於取消通道的註冊。

SelectionKey中包含以下屬性:

  • The interest set
  • The ready set
  • The Channel
  • The Selector
  • An attached object (optional)

(1)Interest set

就像咱們在前面講到的把Channel註冊到Selector來監聽感興趣的事件,interest set就是你要選擇的感興趣的事件的集合。你能夠經過SelectionKey對象來讀寫interest set:

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

經過上面例子能夠看到,咱們能夠經過用AND 和SelectionKey 中的常量作運算,從SelectionKey中找到咱們感興趣的事件。

(2)Ready Set

ready set 是通道已經準備就緒的操做的集合。在一次選Selection以後,你應該會首先訪問這個ready set。Selection將在下一小節進行解釋。能夠這樣訪問ready集合:

int readySet = selectionKey.readyOps();

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

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

(3)Channel 和 Selector

咱們能夠經過SelectionKey得到Selector和註冊的Channel:

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

(4)Attach一個對象

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

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

還能夠在用register()方法向Selector註冊Channel的時候附加對象。如:

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

4.關於SelectedKeys()

生產系統中通常會額外進行就緒狀態檢查

一旦調用了select()方法,它就會返回一個數值,表示一個或多個通道已經就緒,而後你就能夠經過調用selector.selectedKeys()方法返回的SelectionKey集合來得到就緒的Channel。請看演示方法:

Set<SelectionKey> selectedKeys = selector.selectedKeys();

當你經過Selector註冊一個Channel時,channel.register()方法會返回一個SelectionKey對象,這個對象就表明了你註冊的Channel。這些對象能夠經過selectedKeys()方法得到。你能夠經過迭代這些selected key來得到就緒的Channel,下面是演示代碼:

Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> 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.remove();
}

這個循環遍歷selected key的集合中的每一個key,並對每一個key作測試來判斷哪一個Channel已經就緒。

請注意循環中最後的keyIterator.remove()方法。Selector對象並不會從本身的selected key集合中自動移除SelectionKey實例。咱們須要在處理完一個Channel的時候本身去移除。當下一次Channel就緒的時候,Selector會再次把它添加到selected key集合中。

SelectionKey.channel()方法返回的Channel須要轉換成你具體要處理的類型,好比是ServerSocketChannel或者SocketChannel等等。

(4)NIO多路複用

主要步驟和元素:

  • 首先,經過 Selector.open() 建立一個 Selector,做爲相似調度員的角色。

  • 而後,建立一個 ServerSocketChannel,而且向 Selector 註冊,經過指定 SelectionKey.OP_ACCEPT,告訴調度員,它關注的是新的鏈接請求。

  • 注意,爲何咱們要明確配置非阻塞模式呢?這是由於阻塞模式下,註冊操做是不容許的,會拋出 IllegalBlockingModeException 異常。

  • Selector 阻塞在 select 操做,當有 Channel 發生接入請求,就會被喚醒。

  • 在 具體的 方法中,經過 SocketChannel 和 Buffer 進行數據操做

IO 都是同步阻塞模式,因此須要多線程以實現多任務處理。而 NIO 則是利用了單線程輪詢事件的機制,經過高效地定位就緒的 Channel,來決定作什麼,僅僅 select 階段是阻塞的,能夠有效避免大量客戶端鏈接時,頻繁線程切換帶來的問題,應用的擴展能力有了很是大的提升

回到頂部

3、NIO2(異步、非阻塞)

AIO是異步IO的縮寫,雖然NIO在網絡操做中,提供了非阻塞的方法,可是NIO的IO行爲仍是同步的。對於NIO來講,咱們的業務線程是在IO操做準備好時,獲得通知,接着就由這個線程自行進行IO操做,IO操做自己是同步的。

可是對AIO來講,則更加進了一步,它不是在IO準備好時再通知線程,而是在IO操做已經完成後,再給線程發出通知。所以AIO是不會阻塞的,此時咱們的業務邏輯將變成一個回調函數,等待IO操做完成後,由系統自動觸發。

與NIO不一樣,當進行讀寫操做時,只須直接調用API的read或write方法便可。這兩種方法均爲異步的,對於讀操做而言,當有流可讀取時,操做系統會將可讀的流傳入read方法的緩衝區,並通知應用程序;對於寫操做而言,當操做系統將write方法傳遞的流寫入完畢時,操做系統主動通知應用程序。 便可以理解爲,read/write方法都是異步的,完成後會主動調用回調函數。 在JDK1.7中,這部份內容被稱做NIO.2,主要在Java.nio.channels包下增長了下面四個異步通道:

  • AsynchronousSocketChannel
  • AsynchronousServerSocketChannel
  • AsynchronousFileChannel
  • AsynchronousDatagramChannel

在AIO socket編程中,服務端通道是AsynchronousServerSocketChannel,這個類提供了一個open()靜態工廠,一個bind()方法用於綁定服務端IP地址(還有端口號),另外還提供了accept()用於接收用戶鏈接請求。在客戶端使用的通道是AsynchronousSocketChannel,這個通道處理提供open靜態工廠方法外,還提供了read和write方法。

在AIO編程中,發出一個事件(accept read write等)以後要指定事件處理類(回調函數),AIO中的事件處理類是CompletionHandler<V,A>,這個接口定義了以下兩個方法,分別在異步操做成功和失敗時被回調。

void completed(V result, A attachment);

void failed(Throwable exc, A attachment);

相關文章
相關標籤/搜索