概述
在我們學習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 抽象、輸入輸出流等。交互方式是同步、阻塞的方式,也就是說,在讀取輸入流或者寫入輸出流時,在讀、寫動作完成之前,線程會一直阻塞在那里,它們之間的調用是可靠的線性順序。
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、分類
按操作數據分為:字節流(InputStream、OutputStream)和字符流(Reader、Writer)
按流向分:輸入流(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(Throwable exc, A attachment);