IO操做包括:對硬盤的讀寫、對socket的讀寫以及外設的讀寫。 java
一個完整的IO讀請求操做包括兩個階段:編程
1)查看內核數據是否就緒;設計模式
2)進行數據拷貝(內核(內核態)將數據拷貝到用戶線程(用戶態))。服務器
當用戶線程發起一個IO請求操做(以讀請求操做爲例),內核會去查看要讀取的數據是否就緒,對於阻塞IO來講,若是數據沒有就緒,則會一直在那等待,直到數據就緒。對於非阻塞IO來講,若是數據沒有就緒,則會返回一個標誌信息告知用戶線程當前要讀的數據沒有就緒,當數據就緒以後,便將數據拷貝到用戶線程,這樣才完成了一個完整的IO讀請求操做。網絡
阻塞(blocking IO)和非阻塞(non-blocking IO)的區別: 就在於第一個階段,若是數據沒有就緒,在查看數據是否就緒的過程當中是一直等待,仍是直接返回一個標誌信息。多線程
Java中傳統的IO都是阻塞IO,好比經過socket來讀數據,調用read()方法以後,若是數據沒有就緒,當前線程就會一直阻塞在read方法調用那裏,直到有數據才返回。異步
而若是是非阻塞IO的話,當數據沒有就緒,read()方法返回一個標誌信息,告知當前線程數據沒有就緒,因此用戶線程會一直輪詢斷定該返回標記等待最終的已就緒標誌。socket
同步IO和異步IO模型是針對用戶線程和內核的交互來講的:async
對於同步IO:當用戶發出IO請求操做以後,若是數據沒有就緒,須要經過用戶線程或內核不斷地去輪詢數據是否就緒,當數據就緒時,再將數據從內核拷貝到用戶線程;函數
而異步IO:只有IO請求操做的發出是由用戶線程來進行的,IO操做的兩個階段都是由內核自動完成,而後發送通知告知用戶線程IO操做已經完成。也就是說在異步IO中不會對用戶線程產生任何阻塞。 因此說異步IO必需要有操做系統的底層支持!
在《Unix網絡編程》一書中提到了五種IO模型,分別是:阻塞IO、非阻塞IO、多路複用IO、信號驅動IO以及異步IO。
non-blocking IO:雖然進程大部分時間都不會被block,可是它仍然要求進程去主動的check,而且當數據準備完成之後,也須要進程主動的再次調用recvfrom來將數據拷貝到用戶內存。
asynchronous IO:它就像是用戶進程將整個IO操做交給了他人(kernel)完成,而後他人作完後發信號通知。在此期間,用戶進程不須要去檢查IO操做的狀態,也不須要主動的去拷貝數據。
IO multiplexing : 阻塞的用戶發起IO
最傳統的一種IO模型,即在讀寫數據過程當中會發生阻塞現象。
用戶線程發出IO請求以後,內核會去查看數據是否就緒,若是沒有就緒就會一直等待,而用戶線程就會處於阻塞狀態,用戶線程交出CPU。當數據就緒以後,內核會將數據拷貝到用戶線程,並返回結果給用戶線程,用戶線程才解除block狀態。
當用戶線程發起一個read操做後,並不須要等待,而是立刻就獲得了一個結果。若是結果是一個error時,它就知道數據尚未準備好,因而它能夠再次發送read操做。一旦內核中的數據準備好了,而且又再次收到了用戶線程的請求,那麼它立刻就將數據拷貝到了用戶線程,而後返回。因此事實上,在非阻塞IO模型中,用戶線程須要不斷地詢問內核數據是否就緒,也就說非阻塞IO不會交出CPU,而會一直佔用CPU(弊端:可能CPU佔用率會很是高) (有些像java的自旋鎖)。
Java NIO實際上就是多路複用IO。
在多路複用IO模型中,會有一個線程不斷去輪詢多個socket的狀態,只有當socket真正有讀寫事件時,才真正調用實際的IO讀寫操做,若是沒有事件,則一直阻塞在那裏,所以這種方式會致使用戶線程(接收socket讀寫事件的線程)的阻塞。
優點:只須要使用一個線程就能夠管理多個socket,系統不須要創建新的進程或者線程,也沒必要維護這些線程和進程,而且只在真正有socket讀寫事件進行時,纔會使用IO資源,因此大大減小了資源佔用。
多路複用IO比非阻塞IO模型的效率高緣由之一是: 在非阻塞IO中,不斷地詢問socket狀態時經過用戶線程去進行的;而在多路複用IO中,輪詢每一個socket狀態是內核在進行的,這個效率要比用戶線程要高的多。
I/O 多路複用的特色是經過一種機制一個進程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select()函數就能夠返回。
不過要注意的是,多路複用IO模型是經過輪詢的方式來檢測是否有事件到達,而且對到達的事件逐一進行響應。所以對於多路複用IO模型來講,一旦事件響應體很大,那麼就會致使後續的事件遲遲得不處處理,而且會影響新的事件輪詢。
所以: 多路複用IO比較適合鏈接數比較多、單個事件處理時間短而快的場景!
當用戶線程發起一個IO請求操做,會給對應的socket註冊一個信號函數,而後用戶線程會繼續向後執行;當內核數據就緒時會發送一個信號給用戶線程,用戶線程接收到信號以後,便在信號函數中調用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。
前面四種IO模型實際上都屬於同步IO,只有最後一種是真正的異步IO,由於不管是多路複用IO仍是信號驅動模型,IO操做的第2個階段都是由用戶線程來作的,內核進行數據拷貝的過程都會讓用戶線程阻塞。
在傳統的網絡服務設計模式中,有兩種比較經典的模式:
一種是 多線程,一種是 線程池。
對於多線程模式,也就說來了client,服務器就會新建一個線程來處理該client的讀寫事件,以下圖所示:
這種模式雖然處理起來簡單方便,可是因爲服務器爲每一個client的鏈接都採用一個線程去處理,使得資源佔用很是大
該模式的弊端:
若使用線程池,若是鏈接大可能是長鏈接,所以可能會致使在一段時間內,線程池中的線程都被佔用,那麼當再有用戶請求鏈接時,因爲沒有可用的空閒線程來處理,就會致使客戶端鏈接失敗,從而影響用戶體驗。所以,線程池比較適合大量的短鏈接應用。
在Reactor模式中,會先對每一個client註冊感興趣的事件,而後有一個線程專門去輪詢每一個client是否有事件發生,當有事件發生時,便順序處理每一個事件,當全部事件處理完以後,便再轉去繼續輪詢,以下圖所示:
從這裏能夠看出,上面的五種IO模型中的多路複用IO就是採用Reactor模式。注意,上面的圖中展現的 是順序處理每一個事件,固然爲了提升事件處理速度,能夠經過多線程或者線程池的方式來處理事件. (netty就是分一個bossExecutor與workerExecutor)
在Proactor模式中,當檢測到有事件發生時,會新起一個異步操做,而後交由內核線程去處理,當內核線程完成IO操做以後,發送一個通知告知操做已完成,能夠得知,異步IO模型採用的就是Proactor模式。