天天看點

Java NIO(一):同步、異步、阻塞、非阻塞

目錄

一、同步、異步、阻塞、非阻塞

二、同步IO & 異步IO

三、阻塞IO & 非阻塞IO

四、傳統IO的不足之處

NIO的全稱是non-blocking IO,也就是說這種I/O模型是非阻塞的,就這涉及到并發的問題,主要展現在同步與異步,阻塞與非阻塞。

一、同步、異步、阻塞、非阻塞

通俗來講

以并發的思維來了解:

1、同步:當多個任務要發生時,這些任務必須逐個地進行,一個任務的執行會導緻整個流程的暫時等待,這些事件不是并發地執行的;

2、異步:當多個任務要發生時,這些任務可以并發地執行,一個任務的執行不會導緻整個流程的暫時等待。

3、阻塞:當某個任務在執行過程中發出一個請求操作,但是由于該請求操作需要的條件不滿足,那麼就會一直在那等待,直至條件滿足;

4、非阻塞:當某個任務在執行過程中發出一個請求操作,如果該請求操作需要的條件不滿足,會立即傳回一個标志資訊告知條件不滿足,不會一直處于等待狀态。

以I/O模型的思維來了解:

1、同步:API調用傳回時調用者就知道操作的結果如何了(實際讀取/寫入了多少位元組)。

2、異步:相對于同步,API調用傳回時調用者不知道操作的結果,後面才會回調通知結果。

3、阻塞:當無資料可讀,或者不能寫入所有資料時,挂起目前線程等待。

4、非阻塞:讀取時,可以讀多少資料就讀多少然後傳回,寫入時,可以寫入多少資料就寫入多少然後傳回。

二、同步IO & 異步IO

通常來說,IO操作包括:對硬碟的讀寫、對socket的讀寫以及外設的讀寫。

一個完整的IO讀請求操作包括兩個階段:

1)檢視資料是否就緒;

2)進行資料拷貝(核心将資料拷貝到使用者線程)。

在《Unix網絡程式設計》一書中對同步IO和異步IO的定義是這樣的:

A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes. 

同步I/O操作會導緻請求流程被阻塞,直到I/O操作完成。

An asynchronous I/O operation does not cause the requesting process to be blocked.

異步I/O操作不會導緻請求流程被阻塞。

根據Oracle官網的文檔,同步異步的劃分标準是“調用者是否需要等待I/O操作完成”,這個“等待I/O操作完成”的意思不是指一定要讀取到資料或者說寫入所有資料,而是指真正進行I/O操作時,比如資料在TCP/IP協定棧緩沖區和JVM緩沖區之間傳輸的這段時間,調用者是否要等待。

對于同步IO,當使用者發出IO請求操作之後,如果資料沒有就緒,需要通過使用者線程或者核心不斷地去輪詢資料是否就緒,當資料就緒時,再将資料從核心拷貝到使用者線程;

對于異步IO,隻有IO請求操作的發出是由使用者線程來進行的,IO操作的兩個階段都是由核心自動完成,然後發送通知告知使用者線程IO操作已經完成。也就是說在異步IO中,不會對使用者線程産生任何阻塞。

是以資料拷貝階段是由使用者線程完成還是核心完成便是同步IO和異步IO 差別的關鍵所在。也就是說異步IO必須要有作業系統的底層支援。

三、阻塞IO & 非阻塞IO

而阻塞與非阻塞展現在上述的第一階段:檢視資料是否就緒

對于阻塞IO:如果資料沒有就緒,則會一直在那等待,直到資料就緒;

對于非阻塞IO:如果資料沒有就緒,則會傳回一個标志資訊告知使用者線程目前要讀的資料沒有就緒。當資料就緒之後,便将資料拷貝到使用者線程,這樣才完成了一個完整的IO讀請求操作,

Java中傳統的IO都是阻塞IO,比如通過socket來讀資料,調用read()方法之後,如果資料沒有就緒,目前線程就會一直阻塞在read方法調用那裡,直到有資料才傳回;而如果是非阻塞IO的話,當資料沒有就緒,read()方法應該傳回一個标志資訊,告知目前線程資料沒有就緒,而不是一直在那裡等待。

read() 和 write() 方法都是同步I/O,同步I/O又分為阻塞和非阻塞兩種模式,如果是非阻塞模式,檢測到無資料可讀時,直接就傳回了,并沒有真正執行I/O操作。

總結:Java中實際上有同步阻塞I/O、同步非阻塞I/O(NIO)與異步I/O三種機制(JDK 1.7才開始引入異步 I/O,那稱之為NIO.2)。

四、傳統IO的不足之處

傳統 I/O 是阻塞式I/O,主要問題是系統資源的浪費。比如我們為了讀取一個TCP連接配接的資料,調用 InputStream 的 read() 方法,這會使目前線程被挂起,直到有資料到達才被喚醒,那該線程在資料到達這段時間内,占用着記憶體資源(存儲線程棧)卻無所作為。為了讀取其他連接配接的資料,我們不得不啟動另外的線程。在并發連接配接數量不多的時候,這可能沒什麼問題,然而當連接配接數量達到一定規模,記憶體資源會被大量線程消耗殆盡。另一方面,線程切換需要更改處理器的狀态,比如程式計數器、寄存器的值,是以非常頻繁的在大量線程之間切換,同樣是一種資源浪費。

參考資料

https://blog.csdn.net/javaxuexi123/article/details/81910644

https://www.cnblogs.com/dolphin0520/p/3916526.html