java IO、NIO、AIO詳解
正文
概述
在我們學習Java的IO流之前,我們都要了解幾個關鍵詞
- 同步與非同步(synchronous/asynchronous):同步是一種可靠的有序執行機制,當我們進行同步操作時,後續的任務是等待當前呼叫返回,才會進行下一步;而非同步則相反,其他任務不需要等待當前呼叫返回,通常依靠事件、回撥等機制來實現任務間次序關係
- 阻塞與非阻塞:在進行阻塞操作時,當前執行緒會處於阻塞狀態,無法從事其他任務,只有當條件就緒才能繼續,比如ServerSocket新連線建立完畢,或者資料讀取、寫入操作完成;而非阻塞
同步和非同步的概念:實際的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 抽象、輸入輸出流等。互動方式是同步、阻塞的方式,也就是說,在讀取輸入流或者寫入輸出流時,在讀、寫動作完成之前,執行緒會一直阻塞在那裡,它們之間的呼叫是可靠的線性順序。
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 操作基於事件和回撥機制,可以簡單理解為,應用操作直接返回,而不會阻塞在那裡,當後臺處理完成,作業系統會通知相應執行緒進行後續工作。
一、IO流(同步、阻塞)
1、概述
IO流簡單來說就是input和output流,IO流主要是用來處理裝置之間的資料傳輸,Java IO對於資料的操作都是通過流實現的,而java用於操作流的物件都在IO包中。
2、分類
按操作資料分為:位元組流(Reader、Writer)和字元流(InputStream、OutputStream)
按流向分:輸入流(Reader、InputStream)和輸出流(Writer、OutputStream)
3、字元流
概述
只用來處理文字資料
資料最常見的表現形式是檔案,字元流用來操作檔案的子類一般是FileReader和FileWriter
字元流讀寫檔案注意事項:
- 寫入檔案必須要用flush()重新整理
- 用完流記得要關閉流
- 使用流物件要丟擲IO異常
- 定義檔案路徑時,可以用"/"或者"\"
- 在建立一個檔案時,如果目錄下有同名檔案將被覆蓋
- 在讀取檔案時,必須保證該檔案已存在,否則丟擲異常
字元流的緩衝區
- 緩衝區的出現是為了提高流的操作效率而出現的
- 需要被提高效率的流作為引數傳遞給緩衝區的建構函式
- 在緩衝區中封裝了一個數組,存入資料後一次取出
4、位元組流
概述
用來處理媒體資料
位元組流讀寫檔案注意事項:
- 位元組流和字元流的基本操作是相同的,但是想要操作媒體流就需要用到位元組流
- 位元組流因為操作的是位元組,所以可以用來操作媒體檔案(媒體檔案也是以位元組儲存的)
- 輸入流(InputStream)、輸出流(OutputStream)
- 位元組流操作可以不用重新整理流操作
- InputStream特有方法:int available()(返回檔案中的位元組個數)
位元組流的緩衝區
位元組流緩衝區跟字元流緩衝區一樣,也是為了提高效率
5、Java Scanner類
Java 5添加了java.util.Scanner類,這是一個用於掃描輸入文字的新的實用程式
關於nextInt()、next()、nextLine()的理解
nextInt():只能讀取數值,若是格式不對,會丟擲java.util.InputMismatchException異常
next():遇見第一個有效字元(非空格,非換行符)時,開始掃描,當遇見第一個分隔符或結束符(空格或換行符)時,結束掃描,獲取掃描到的內容
nextLine():可以掃描到一行內容並作為字串而被捕獲到
關於hasNext()、hasNextLine()、hasNextxxx()的理解
就是為了判斷輸入行中是否還存在xxx的意思
與delimiter()有關的方法
應該是輸入內容的分隔符設定,
二、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 階段是阻塞的,可以有效避免大量客戶端連線時,頻繁執行緒切換帶來的問題,應用的擴充套件能力有了非常大的提高
三、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(Throwabl