阻塞和非阻塞
阻塞的時候線程會被掛起
阻塞:
當數據還沒準備好時,調用了阻塞的方法,則線程會被掛起,會讓出CPU時間片,此時是無法處理過來的請求,需要等待其他線程來進行喚醒,該線程才能進行後續操作或者處理其他請求。
非阻塞:
意味着,當數據還沒準備好的時候,即便我調用了阻塞方法,該線程也不會被掛起,後續的請求也能夠被處理。
同步
同步和異步跟串行和並行非常形似。
假設在一個場景下:完成一個大任務需要4個小任務。
同步的做法:需要依次4個步驟,注意這裏是依次,也就是說完成這個步驟,需要先完成前置步驟,也就是說下一個步驟是要看上一個步驟的執行結果。
異步的做法:可以同時進行4個步驟,無需等待其他步驟的執行結果。
阻塞和同步的最本質差別在於:
即便是同步,在等待的過程中,線程是不會被掛起,也不需要讓出CPU時間片的,
在IO中的體現
網絡編程的基本模型是:Client/Server模型
兩個進程之間要相互通信,其中服務端需要提供位置信息,讓客戶端找到自己。服務端提供IP地址和監聽的端口。
客戶端拿着這些信息去向服務端發起建立連接請求,通過三次握手成功建立連接後,客戶端就可以通過socket
向服務器發送和接受消息。
BIO
BIO通信模型採用的是典型的:一請求一應答通信模型
採用BIO通信模型的服務端,通常會由一個獨立的Acceptor
線程負責監聽客戶端的連接。
他不負責處理請求,他只是起到一個委派工作的作用,當他接收到請求之後,會爲每個客戶端創建一個新的線程進行鏈路處理。
處理完之後,通過輸出流,返回應答給客戶端,然後線程被銷燬,資源被回收。
該模型的最大問題就是缺乏彈性伸縮能力,服務端的線程個數和客戶端的併發訪問數是1:1
的關係。
由於線程是Java虛擬機非常寶貴的資源,當線程書膨脹之後,系統的性能會隨着併發量增加呈正比的趨勢下降。
而且會有OOM
的風險,當沒有內存空間創建線程時,就無法處理客戶端請求,最終導致進程宕機或卡死,無法對外提供服務。
最大的問題就是:每當有一個客戶端請求接入時,就會創建一個線程來處理請求。
爲了改進這個一線程一連接模型,後面又演進出通過:
- 線程池
- 消息隊列
來實現1個或者多個線程處理N個客戶端的模型。
在這裏,無論是線程池和消息隊列,都是解決內存空間,線程的問題,並沒有實質性地改變同步阻塞通信本質問題
所以這種優化版本的BIO也被稱爲是僞異步。
僞異步IO
採用線程池和任務隊列可以實現一種:僞異步的IO通信
將客戶端的請求封裝成一個Task
(該任務實現java.lang.Runnable接口),投遞到消息隊列中。
如果通過線程池維護一堆處理線程,去消費隊列中的消息。
處理完畢之後,再去通過客戶端就可以了,他的資源是可控的,無論客戶端的請求量是多少,也不會發生變化,同樣這也是他的缺點之一。
建立連接的accpet
方法、讀取數據的read
方法都是阻塞。
這就意味着,如果有一方處理請求或者發出請求的比較慢,或者是網絡傳輸比較慢,那麼都會影響對方。
當調用OutputStream的write
方法寫輸出流的時候,它將會被阻塞,直到所有要發送的字節全部寫入完畢,或者發生異常。
在TCP/IP中,當消息的接收方處理緩慢的時候,由於消息滑動窗口的存在,那麼它的接收窗口就會變小,就是那個TCP window size
。
如果這裏採用同步阻塞IO,並且write
操作被阻塞很久,直到TCP window size
大於0或者發生IO異常了。
那麼通信對方返回應答時間過長會引起的級聯故障:
- 線程問題:假如所有的可用線程都被故障服務器阻塞,那麼後續所有的IO消息都將被隊列中排隊。
-
隊列問題:如果隊列採用的是
有界隊列
,隊列滿了之後那麼就會無法後續處理請求;如果採用的是無界隊列
,那麼會有OOM風險。
NIO
NIO,官方叫法是
new IO
,因爲它相對於之前出的java.io包是新增的但是之前老的IO庫都是阻塞的,New IO類庫目標就是爲了讓Java支持非阻塞IO,所有更多的人稱爲
Non-Block IO
緩衝區Buffer
Buffer是一個對象,通常是ByteBuffer類型
任何時候操作NIO中的數據,都需要經過緩衝區。
在NIO
庫裏,所有數據操作是用緩衝區處理的。
- 讀取數據時,是直接讀到緩衝區中(這裏並沒有直接讀到某個地方,而是都放到緩衝區中)
- 寫入數據時,寫入到緩衝區
緩衝區實質上是一個數組,通常是一個字節數組ByteBuffer
,自身還需要維護讀寫位置,可以用指針或者偏移量來實現。
除了ByteBuffer還有其他基本類型緩衝區:
-
CharBuffer
:字符緩衝區 -
ShortBuffer
:短整型緩衝區 -
IntBuffer
:整形緩衝區 -
LongBuffer
:長整型緩衝區 -
DoubleBuffer
:雙精度緩衝區
通常是用ByteBuffer
通道Channel
網絡數據通過Channel讀取和寫入
Channel通道和Stream流最大的區別在於:
-
Channel
的數據流向是雙向的 -
Stream
的數據流向是單向的
這就意味着:使用Channel,可以同時進行讀和寫,他是全雙工模型。(可以聯想到HTTP1.1
HTTP2.0
HTTP3.0 ``websocket
)
多路複用器Selector
Selector是NIO編程的基礎
Selector
會不斷輪詢註冊在其上的Channel
。
如果某個Channel發生讀寫事件,就代表這個Channel是就緒狀態,會被Selector輪詢出來。
然後根據SelectionKey
可以獲取就緒Channel的集合,進行後續IO操作。
一個Selector可以輪詢多個Channel,JDK是基於epoll代替傳統的select,所以不受句柄fd的限制。
意味着,一個線程負責Selector的輪詢千萬個客戶端,
AIO
NIO2.0
引入了新的異步通道的概念,並提供了異步文件通道和異步套接字通道的實現
- 通過java.util.concurrent.
Future
類來表示異步操作的結果。 - 在執行異步操作的時候傳入一個java.nio.channels
CompletionHandler接口的實現類作爲操作完成的回調
NIO2.0
的異步socket通道是真正的異步非阻塞IO。
-
同步socket channel:
SocketServerChannel
-
異步socket channel:
AsynchronousServerSocketChannel
它不需要通過多路複用器(selector
)對註冊到裏面的通過進行輪詢操作,就可以實現異步讀寫。
AIO和NIO最大的區別在於:異步Socket Channel是被動執行對象
- NIO需要我們把channel註冊到selector上進行順序掃描、輪詢
- AIO則是通過
Future
類,實現回調方法:completed、failed
4種IO對比
IO模型主要是探討2個維度:
- 同步/異步
- 阻塞/非阻塞
同步/異步的判斷標準主要是:Channel
的問題
阻塞/非阻塞的判斷標準主要是:selector
的問題
阻塞的關鍵點在於:建立連接和數據傳輸
BIO(阻塞)意味着在完成建立連接(accpet
)動作之後,才能進行後續操作
NIO(非阻塞)在處理客戶端的連接時,可以將對應的channel註冊到Selector上,此時我不管他好了沒有,我有Selecotr
來幫我去掃就緒態的channel,所以他是非阻塞的
異步非阻塞IO
異步非阻塞IO:
AIO
有的人也叫JDK1.4
推出的NIO爲異步非阻塞IO
但是嚴格來說,它只能被稱爲是非阻塞IO,並不是真正意義上的異步
前期selector
的底層是通過select/poll來實現的,雖然是用epoll替代了select/poll,上層的API沒有變化,只是一次NIO的性能優化,仍舊沒有改變IO的模型
在JDK1.7
提供的NIO2.0
新增了:異步套接字通道,他纔是真正的異步IO。
多路複用器Selector
Selector的核心功能:就是用來輪詢註冊在它上面的Channel
當發現某個就緒態的Channel,就會找出他的SelectionKey
,然後進行後續的IO操作。
前期的時候JDK1.4,selector底層是基於select/poll技術實現
後面優化,使用epoll來代替
僞異步IO
只是在線程層面上進行了一次優化,IO模型並沒有改變
通過處理任務Task隊列+線程池處理請求的方式來優化資源
解決了BIO的線程和請求:1對1的關係