文章目錄
- 阻塞 IO 模型
- 非阻塞 IO 模型
- 多路複用 IO 模型
- 信号驅動 IO 模型
- 異步 IO 模型
- java NIO
-
- NIO 的緩沖區
- NIO 的非阻塞
- Channel
- Buffer
- Selector
阻塞 IO 模型
最傳統的一種 IO 模型,即在讀寫資料過程中會發生阻塞現象。當使用者線程發出 IO 請求之後,核心會去檢視資料是否就緒,如果沒有就緒就會等待資料就緒,而使用者線程就會處于阻塞狀态,使用者線程交出 CPU。當資料就緒之後,核心會将資料拷貝到使用者線程,并傳回結果給使用者線程,使用者線程才解除 block 狀态。典型的阻塞 IO 模型的例子為:data = socket.read();如果資料沒有就緒,就會一直阻塞在 read 方法。
非阻塞 IO 模型
當使用者線程發起一個 read 操作後,并不需要等待,而是馬上就得到了一個結果。如果結果是一個error 時,它就知道資料還沒有準備好,于是它可以再次發送 read 操作。一旦核心中的資料準備好了,并且又再次收到了使用者線程的請求,那麼它馬上就将資料拷貝到了使用者線程,然後傳回。是以事實上,在非阻塞 IO 模型中,使用者線程需要不斷地詢問核心資料是否就緒,也就說非阻塞 IO不會交出 CPU,而會一直占用 CPU。典型的非阻塞 IO 模型一般如下:
while(true){
data = socket.read();
if(data!= error){
處理資料
break;
}
}
但是對于非阻塞 IO 就有一個非常嚴重的問題,在 while 循環中需要不斷地去詢問核心資料是否就
緒,這樣會導緻 CPU 占用率非常高,是以一般情況下很少使用 while 循環這種方式來讀取資料。
多路複用 IO 模型
多路複用 IO 模型是目前使用得比較多的模型。Java NIO 實際上就是多路複用 IO。在多路複用 IO
模型中,會有一個線程不斷去輪詢多個 socket 的狀态,隻有當 socket 真正有讀寫事件時,才真
正調用實際的 IO 讀寫操作。因為在多路複用 IO 模型中,隻需要使用一個線程就可以管理多個socket,系統不需要建立新的程序或者線程,也不必維護這些線程和程序,并且隻有在真正有socket 讀寫事件進行時,才會使用 IO 資源,是以它大大減少了資源占用。在 Java NIO 中,是通過 selector.select()去查詢每個通道是否有到達事件,如果沒有事件,則一直阻塞在那裡,是以這種方式會導緻使用者線程的阻塞。多路複用 IO 模式,通過一個線程就可以管理多個 socket,隻有當socket 真正有讀寫事件發生才會占用資源來進行實際的讀寫操作。是以,多路複用 IO 比較适合連接配接數比較多的情況。
多路複用 IO 為何比非阻塞 IO 模型的效率高是因為在非阻塞 IO 中,不斷地詢問 socket 狀态
時通過使用者線程去進行的,而在多路複用 IO 中,輪詢每個 socket 狀态是核心在進行的,這個效
率要比使用者線程要高的多。
不過要注意的是,多路複用 IO 模型是通過輪詢的方式來檢測是否有事件到達,并且對到達的事件
逐一進行響應。是以對于多路複用 IO 模型來說,一旦事件響應體很大,那麼就會導緻後續的事件
遲遲得不到處理,并且會影響新的事件輪詢。
信号驅動 IO 模型
在信号驅動 IO 模型中,當使用者線程發起一個 IO 請求操作,會給對應的 socket 注冊一個信号函數,然後使用者線程會繼續執行,當核心資料就緒時會發送一個信号給使用者線程,使用者線程接收到信号之後,便在信号函數中調用 IO 讀寫操作來進行實際的 IO 請求操作。
異步 IO 模型
異步 IO 模型才是最理想的 IO 模型,在異步 IO 模型中,當使用者線程發起 read 操作之後,立刻就
可以開始去做其它的事。而另一方面,從核心的角度,當它受到一個 asynchronous read 之後,
它會立刻傳回,說明 read 請求已經成功發起了,是以不會對使用者線程産生任何 block。然後,内
核會等待資料準備完成,然後将資料拷貝到使用者線程,當這一切都完成之後,核心會給使用者線程
發送一個信号,告訴它 read 操作完成了。也就說使用者線程完全不需要實際的整個 IO 操作是如何
進行的,隻需要先發起一個請求,當接收核心傳回的成功信号時表示 IO 操作已經完成,可以直接
去使用資料了。
也就說在異步 IO 模型中,IO 操作的兩個階段都不會阻塞使用者線程,這兩個階段都是由核心自動完
成,然後發送一個信号告知使用者線程操作已完成。使用者線程中不需要再次調用 IO 函數進行具體的
讀寫。這點是和信号驅動模型有所不同的,在信号驅動模型中,當使用者線程接收到信号表示資料
已經就緒,然後需要使用者線程調用 IO 函數進行實際的讀寫操作;而在異步 IO 模型中,收到信号
表示 IO 操作已經完成,不需要再在使用者線程中調用 IO 函數進行實際的讀寫操作。
異步 IO 是需要作業系統的底層支援,在 Java 7 中,提供了 Asynchronous IO。
java NIO
NIO 主要有三大核心部分: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 是雙向的,既可以用來進行讀操作,又可以用來進行寫操作。NIO 中的 Channel 的主要實作有:
- FileChannel
- DatagramChannel
- SocketChannel
-
ServerSocketChannel
分别可以對應檔案 IO、UDP 和 TCP(Server 和 Client)。
Buffer
Buffer,故名思意,緩沖區,實際上是一個容器,是一個連續數組。Channel 提供從檔案、網絡讀取資料的管道,但是讀取或寫入的資料都必須經由 Buffer。
用戶端發送資料時,必須先将資料存入 Buffer 中,然後将 Buffer 中的内容寫入通道。服務端這邊接收資料必須通過 Channel 将資料讀入到 Buffer 中,然後再從 Buffer 中取出資料來處理。
在 NIO 中,Buffer 是一個頂層父類,它是一個抽象類,常用的 Buffer 的子類有:
ByteBuffer、ShortBuffer、IntBuffer、 CharBuffer、 LongBuffer、 DoubleBuffer、FloatBuffer、
Selector
Selector 類是 NIO 的核心類,Selector 能夠檢測多個注冊的通道上是否有事件發生,如果有事
件發生,便擷取事件然後針對每個事件進行相應的響應處理。這樣一來,隻是用一個單線程就可
以管理多個通道,也就是管理多個連接配接。這樣使得隻有在連接配接真正有讀寫事件發生時,才會調用
函數來進行讀寫,就大大地減少了系統開銷,并且不必為每個連接配接都建立一個線程,不用去維護
多個線程,并且避免了多線程之間的上下文切換導緻的開銷。