【学习笔记】JAVA IO与NIO(new IO)的对比与不同IO模型的理解
- JAVA IO
- 分類:
- 幾種IO 模型
- 1. 阻塞 IO 模型
- 2. 非阻塞 IO 模型
- JAVA NIO
- 多路復用 IO 模型(即Java中的NIO)
JAVA IO
思維導圖:
分類:
按照流的方向:輸入流(inputStream)和輸出流(outputStream)。
按照實現功能分:節點流(可以從或向一個特定的地方(節點)讀寫數據。如FileReader)和處理流(是對一個已存在的流的連接和封裝,通過所封裝的流的功能調用實現數據讀寫。如BufferedReader。處理流的構造方法總是要帶一個其他的流對象做參數。一個流對象經過其他流的多次包裝,稱為流的鏈接。)
按照處理數據的單位:字節流和字符流。字節流繼承于InputStream 和OutputStream,字符流繼承于
InputStreamReader 和OutputStreamWriter。
一些常見的InputStream子類:
ByteArrayInputStream 字節數組輸?流 : 把內存中的?個緩沖區作為 InputStream 使?PipedInputStream 管道輸?流: 實現了pipe 管道的概念,主要在線程中使?SequenceInputStream 順序輸?流:把多個 InputStream 合并為?個 InputStream
FilterOutputStream 過濾輸?流:其他輸?流的包裝。
ObjectInputStream 反序列化輸?流 : 將之前使? ObjectOutputStream 序列化的原始數據恢復為對象,以流的?式讀取對象
DataInputStream : 數據輸?流允許應?程序以與機器?關?式從底層輸?流中讀取基本 Java 數據類型。
PushbackInputStream 推回輸?流: 緩沖的?個新穎的?法是實現推回 (pushback) 。
Pushback ?于輸?流允許字節被讀取然后返回到流。
幾種IO 模型
1. 阻塞 IO 模型
最傳統的一種 IO 模型,即在讀寫數據過程中會發生阻塞現象。當用戶線程發出 IO 請求之后,內核會去查看數據是否就緒,如果沒有就緒就會等待數據就緒,而用戶線程就會處于阻塞狀態,用戶線程交出CPU。當數據就緒之后,內核會將數據拷貝到用戶線程,并返回結果給用戶線程,用戶線程才解除block 狀態。典型的阻塞 IO 模型的例子為:data = socket.read();如果數據沒有就緒,就會一直阻塞在read 方法。
java中,默認創建的socket都是阻塞的。
在java中發起一個socket的read讀操作的系統調用,流程大致如下:
(1)從Java啟動IO讀的read系統調用開始,用戶線程就進入阻塞狀態。
(2)當系統內核收到read系統調用,就開始準備數據。一開始,數據可能還沒有到達內核緩沖區(例如,還沒有收到一個完整的socket數據包),這個時候內核就要等待。
(3)內核一直等到完整的數據到達,就會將數據從內核緩沖區復制到用戶緩沖區(用戶空間的內存),然后內核返回結果(例如返回復制到用戶緩沖區中的字節數)。
(4)直到內核返回后,用戶線程才會解除阻塞的狀態,重新運行起來。 總之,阻塞IO的特點是:在內核進行IO執行的兩個階段,用戶線程都被阻塞了。 阻塞IO的優點是:應用的程序開發非常簡單;在阻塞等待數據期間,用戶線程掛起。在阻塞期間,用戶線程基本不會占用CPU資源。 阻塞IO的缺點是:一般情況下,會為每個連接配備一個獨立的線程;反過來說,就是一個線程維護一個連接的IO操作。在并發量小的情況下,這樣做沒有什么問題。但是,當在高并發的應用場景下,需要大量的線程來維護大量的網絡連接,內存、線程切換開銷會非常巨大。因此,基本上阻塞IO模型在高并發應用場景下是不可用的。
解釋同步與異步:
同步IO,指的是一種用戶空間與內核空間的IO發起方式,同步IO是指用戶空間的線程是主動發起IO請求的一方,內核空間是被動接受方,異步IO則反過來,是指系統內核是主動發起IO請求的一方,用戶空間的線程是被動接收方。
2. 非阻塞 IO 模型
這里說明一下,同步非阻塞IO,可以簡稱為NIO,但是,它不是Java中的NIO,雖然它們的英文縮寫一樣,希望大家不要混淆。Java的NIO(New IO),對應的不是四種基礎IO模型中的NIO(None Blocking IO)模型,而是另外的一種模型,叫作IO多路復用模型(IO Multiplexing)。
socket連接默認是阻塞模式,在Linux系統下,可以通過設置將socket變成為非阻塞的模式(Non-Blocking)。使用非阻塞模式的IO讀寫,叫作同步非阻塞IO(None Blocking IO),簡稱為NIO模式。在NIO模型中,應用程序一旦開始IO系統調用,會出現以下兩種情況:
(1)在內核緩沖區中沒有數據的情況下,系統調用會立即返回,返回一個調用失敗的信息。
(2)在內核緩沖區中有數據的情況下,是阻塞的,直到數據從內核緩沖復制到用戶進程緩沖。復制完成后,系統調用返回成功
舉個例子。發起一個非阻塞socket的read讀操作的系統調用,流程如下:
(1)在內核數據沒有準備好的階段,用戶線程發起IO請求時,立即返回。所以,為了讀取到最終的數據,用戶線程需要不斷地發起IO系統調用。
(2)內核數據到達后,用戶線程發起系統調用,用戶線程阻塞。內核開始復制數據,它會將數據從內核緩沖區復制到用戶緩沖區(用戶空間的內存),然后內核返回結果(例如返回復制到的用戶緩沖區的字節數)。
(3)用戶線程讀到數據后,才會解除阻塞狀態,重新運行起來。也就是說,用戶進程需要經過多次的嘗試,才能保證最終真正讀到數據,而后繼續執行。 同步非阻塞IO的特點:應用程序的線程需要不斷地進行IO系統調用,輪詢數據是否已經準備好,如果沒有準備好,就繼續輪詢,直到完成IO系統調用為止。 同步非阻塞IO的優點:每次發起的IO系統調用,在內核等待數據過程中可以立即返回。用戶線程不會阻塞,實時性較好。 同步非阻塞IO的缺點:不斷地輪詢內核,這將占用大量的CPU時間,效率低下。 總體來說,在高并發應用場景下,同步非阻塞IO也是不可用的。一般Web服務器不使用這種IO模型。這種IO模型一般很少直接使用,而是在其他IO模型中使用非阻塞IO這一特性。在Java的實際開發中,也不會涉及這種模型。
當用戶線程發起一個read 操作后,并不需要等待,而是馬上就得到了一個結果。如果結果是一個error 時,它就知道數據還沒有準備好,于是它可以再次發送read 操作。一旦內核中的數據準備好了,并且又再次收到了用戶線程的請求,那么它馬上就將數據拷貝到了用戶線程,然后返回。 所以事實上,在非阻塞 IO 模型中,用戶線程需要不斷地詢問內核數據是否就緒,也就說非阻塞 IO 不會交出CPU,而會一直占用CPU。典型的非阻塞IO 模型一般如下:
while(true){ data = socket.read(); if(data!= error){ 處理數據break; } }但是對于非阻塞IO 就有一個非常嚴重的問題,在 while 循環中需要不斷地去詢問內核數據是否就緒,這樣會導致CPU 占用率非常高,因此一般情況下很少使用 while 循環這種方式來讀取數據。
簡單來說,阻塞是指用戶空間(調用線程)一直在等待,而不能干別的事情,非阻塞是指用戶空間(調用線程)拿到內核返回的狀態值就返回自己的空間,IO操作可以干就干,不可以干就干別的事情。
非阻塞IO要求socket被設置為NONBLOCK。
在信號驅動 IO 模型中,當用戶線程發起一個 IO 請求操作,會給對應的socket 注冊一個信號函數,然后用戶線程會繼續執行,當內核數據就緒時會發送一個信號給用戶線程,用戶線程接收到信號之后,便在信號函數中調用 IO 讀寫操作來進行實際的 IO 請求操作。
異步 IO 模型才是最理想的 IO 模型,在異步 IO 模型中,當用戶線程發起read 操作之后,立刻就可以開始去做其它的事。而另一方面,從內核的角度,當它收到一個asynchronous read 之后, 它會立刻返回,說明read 請求已經成功發起了,因此不會對用戶線程產生任何 block。然后,內核會等待數據準備完成,然后將數據拷貝到用戶線程,當這一切都完成之后,內核會給用戶線程發送一個信號,告訴它read 操作完成了。也就說用戶線程完全不需要實際的整個 IO 操作是如何進行的,只需要先發起一個請求,當接收內核返回的成功信號時表示 IO 操作已經完成,可以直接去使用數據了。
這有點類似JAVA中的回調模式,用戶空間的線程向內核空間注冊了各種IO事件的回調函數,由內核去主動調用。
也就說在異步 IO 模型中,IO 操作的兩個階段都不會阻塞用戶線程,這兩個階段都是由內核自動完成,然后發送一個信號告知用戶線程操作已完成。用戶線程中不需要再次調用 IO 函數進行具體的讀寫。這點是和信號驅動模型有所不同的,在信號驅動模型中,當用戶線程接收到信號表示數據 已經就緒,然后需要用戶線程調用 IO 函數進行實際的讀寫操作;而在異步 IO 模型中,收到信號表示 IO 操作已經完成,不需要再在用戶線程中調用 IO 函數進行實際的讀寫操作。
JAVA NIO
多路復用 IO 模型(即Java中的NIO)
多路復用 IO 模型是目前使用得比較多的模型。即經典的Reactor反應器設計模式,有時也稱異步阻塞IO。JAVA的selector選擇器和LINUX的epoll都是這種模型。
如何避免同步非阻塞IO模型中輪詢等待的問題呢?這就是IO多路復用模型。
Java NIO 實際上就是多路復用IO。在多路復用 IO 模型中,會有一個線程不斷去輪詢多個socket 的狀態,只有當 socket 真正有讀寫事件時,才真正調用實際的 IO 讀寫操作。因為在多路復用 IO 模型中,只需要使用一個線程就可以管理多個socket,系統不需要建立新的進程或者線程,也不必維護這些線程和進程,并且只有在真正有socket 讀寫事件進行時,才會使用 IO 資源,所以它大大減少了資源占用。在Java NIO 中,是通過 selector.select()去查詢每個通道是否有到達事件,如果沒有事件,則一直阻塞在那里,因此這種方式會導致用戶線程的阻塞。多路復用 IO 模式,通過一個線程就可以管理多個socket,只有當socket 真正有讀寫事件發生才會占用資源來進行實際的讀寫操作。因此,多路復用 IO 比較適合連接數比較多的情況。
舉個例子來說明IO多路復用模型的流程。發起一個多路復用IO的read讀操作的系統調用,流程如下:
(1)選擇器注冊。在這種模式中,首先,將需要read操作的目標socket網絡連接,提前注冊到select/epoll選擇器中,Java中對應的選擇器類是Selector類。然后,才可以開啟整個IO多路復用模型的輪詢流程。
2)就緒狀態的輪詢。通過選擇器的查詢方法,查詢注冊過的所有socket連接的就緒狀態。通過查詢的系統調用,內核會返回一個就緒的socket列表。當任何一個注冊過的socket中的數據準備好了,內核緩沖區有數據(就緒)了,內核就將該socket加入到就緒的列表中。 當用戶進程調用了select查詢方法,那么整個線程會被阻塞掉。
(3)用戶線程獲得了就緒狀態的列表后,根據其中的socket連接,發起read系統調用,用戶線程阻塞。內核開始復制數據,將數據從內核緩沖區復制到用戶緩沖區。
(4)復制完成后,內核返回結果,用戶線程才會解除阻塞的狀態,用戶線程讀取到了數據,繼續執行。 IO多路復用模型的流程如圖:
另外多路復用 IO 為何比非阻塞 IO 模型的效率高是因為在非阻塞 IO 中,不斷地詢問socket 狀態時通過用戶線程去進行的,而在多路復用 IO 中,輪詢每個 socket 狀態是內核在進行的,這個效率要比用戶線程要高的多。
IO多路復用模型的特點:IO多路復用模型的IO涉及兩種系統調用(System Call),另一種是select/epoll(就緒查詢),一種是IO操作。IO多路復用模型建立在操作系統的基礎設施之上,即操作系統的內核必須能夠提供多路分離的系統調用select/epoll。 多路復用IO也需要輪詢。負責select/epoll狀態查詢調用的線程,需要不斷地進行select/epoll輪詢,查找出達到IO操作就緒的socket連接。 IO多路復用模型與同步非阻塞IO模型是有密切關系的。對于注冊在選擇器上的每一個可以查詢的socket連接,一般都設置成為同步非阻塞模型。僅是這一點,對于用戶程序而言是無感知的。
IO多路復用模型的優點:與一個線程維護一個連接的阻塞IO模式相比,使用select/epoll的最大優勢在于,一個選擇器查詢線程可以同時處理成千上萬個連接(Connection)。系統不必創建大量的線程,也不必維護這些線程,從而大大減小了系統的開銷。 在Linux系統上,使用的是epoll系統調用。
不過要注意的是,多路復用 IO 模型是通過輪詢的方式來檢測是否有事件到達,并且對到達的事件逐一進行響應。因此對于多路復用 IO 模型來說,一旦事件響應體很大,那么就會導致后續的事件遲遲得不到處理,并且會影響新的事件輪詢。
NIO 的特點:事件驅動模型、單線程處理多任務、非阻塞 I/O,I/O 讀寫不再阻塞,而是返回 0、基于 block 的傳輸比基于流的傳輸更高效、更高級的 IO 函數 zero-copy、IO 多路復用大大提高了 Java 網絡應用的可伸縮性和實用性?;?Reactor 線程模型。
NIO(new io) 主要有三大核心部分:Channel(通道),Buffer(緩沖區), Selector。傳統 IO 基于字節流和字符流進行操作,而NIO 基于 Channel 和 Buffer(緩沖區)進行操作,數據總是從通道讀取到緩沖區中,或者從緩沖區寫入到通道中。Selector(選擇區)用于監聽多個通道的事件(比如:連接打開, 數據到達)。因此,單個線程可以監聽多個數據通道。
NIO 和傳統 IO 之間第一個最大的區別是,IO 是面向流的,NIO 是面向緩沖區的。
NIO 的緩沖區
Java IO 面向流意味著每次從流中讀一個或多個字節,直至讀取所有字節,它們沒有被緩存在任何地方。此外,它不能前后移動流中的數據。如果需要前后移動從流中讀取的數據,需要先將它緩 存到一個緩沖區。NIO 的緩沖導向方法不同。數據讀取到一個它稍后處理的緩沖區,需要時可在緩沖區中前后移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩沖區中包含所 有您需要處理的數據。而且,需確保當更多的數據讀入緩沖區時,不要覆蓋緩沖區里尚未處理的 數據。
NIO 的非阻塞
IO 的各種流是阻塞的。這意味著,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入。該線程在此期間不能再干任何事情了。 NIO 的非阻塞模式, 使一個線程從某通道發送請求讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什么都不會獲取。而不是保持線程阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其他的事情。 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。 線程通常將非阻塞 IO 的空閑時間用于在其它通道上執行 IO 操作,所以一個單獨的線程現在可以管理多個輸入和輸出通道(channel)。
Channel
首先說一下Channel,國內大多翻譯成“通道”。Channel 和 IO 中的 Stream(流)是差不多一個等級的。只不過 Stream 是單向的,譬如:InputStream, OutputStream,而 Channel 是雙向的,既可以用來進行讀操作,又可以用來進行寫操作。表示 IO 源與目標打開的連接,是雙向的,但不能直接訪問數據,只能與 Buffer進行交互。通過源碼可知,FileChannel 的 read 方法和 write 方法都導致數據復制了兩次.
NIO 中的 Channel 的主要實現有:
1. FileChannel
2. DatagramChannel
3. SocketChannel
4. ServerSocketChannel
這里看名字就可以猜出個所以然來:分別可以對應文件 IO、UDP 和 TCP(Server 和 Client)。
Buffer,故名思意,緩沖區,實際上是一個容器,是一個連續數組。Channel 提供從文件、網絡讀取數據的渠道,但是讀取或寫入的數據都必須經由 Buffer。
上面的圖描述了從一個客戶端向服務端發送數據,然后服務端接收數據的過程??蛻舳税l送數據時,必須先將數據存入 Buffer 中,然后將 Buffer 中的內容寫入通道。服務端這邊接收數據必須通過 Channel 將數據讀入到 Buffer 中,然后再從 Buffer 中取出數據來處理。
在 NIO 中,Buffer 是一個頂層父類,它是一個抽象類,常用的 Buffer 的子類有: ByteBuffer、IntBuffer、 CharBuffer、 LongBuffer、 DoubleBuffer、FloatBuffer、ShortBuffer
Selector 類是 NIO 的核心類,Selector 能夠檢測多個注冊的通道上是否有事件發生,如果有事件發生,便獲取事件然后針對每個事件進行相應的響應處理。這樣一來,只是用一個單線程就可以管理多個通道,也就是管理多個連接。這樣使得只有在連接真正有讀寫事件發生時,才會調用函數來進行讀寫,就大大地減少了系統開銷,并且不必為每個連接都創建一個線程,不用去維護多個線程,并且避免了多線程之間的上下文切換導致的開銷。open 方法可創建 Selector,register 方法向多路復用器器注冊通道,可以監聽的事件類型:讀、寫、連接、accept。注冊事件后會產
生一個 SelectionKey:它表示 SelectableChannel 和 Selector 之間的注冊關系,wakeup 方法:使尚未返回的第一個選擇操作立即返回,喚醒的原因是:注冊了新的 channel 或者事
件;channel 關閉,取消注冊;優先級更高的事件觸發(如定時器事件),希望及時處理。
總結
以上是生活随笔為你收集整理的【学习笔记】JAVA IO与NIO(new IO)的对比与不同IO模型的理解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【java作业】2、 编写两个函数,分别
- 下一篇: 直接输入地址访问服务器上的静态资源