阻塞和非阻塞
阻塞的時候線程會被掛起
阻塞:
當數據還沒準備好時,調用了阻塞的方法,則線程會被掛起,會讓出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的關係