1. 程式人生 > >2019年面試必備:最新Java核心知識點(3)—JAVA多執行緒併發(上)

2019年面試必備:最新Java核心知識點(3)—JAVA多執行緒併發(上)

 

核心知識——JVM

jvm基本概念:

JVM 是可執行 Java 程式碼的假想計算機 ,包括一套位元組碼指令集、一組暫存器、一個棧、 一個垃圾回收,堆 和 一個儲存方法域JVM 是執行在作業系統之上的,它與硬體沒有直接 的互動。

執行過程:

我們都知道 Java 原始檔,通過編譯器,能夠生產相應的.Class 檔案,也就是位元組碼檔案, 而位元組碼檔案又通過 Java 虛擬機器中的直譯器,編譯成特定機器上的機器碼 。

也就是如下:

① Java 原始檔—->編譯器—->位元組碼檔案

② 位元組碼檔案—->JVM—->機器碼

每一種平臺的直譯器是不同的,但是實現的虛擬機器是相同的,這也就是 Java 為什麼能夠 跨平臺的原因了 ,當一個程式從開始執行,這時虛擬機器就開始例項化了,多個程式啟動就會 存在多個虛擬機器例項。程式退出或者關閉,則虛擬機器例項消亡,多個虛擬機器例項之間資料不能共享。 

2.1.執行緒

這裡所說的執行緒指程式執行過程中的一個執行緒實體。JVM 允許一個應用併發執行多個執行緒。 Hotspot JVM 中的 Java 執行緒與原生作業系統執行緒有直接的對映關係。當執行緒本地儲存、緩 衝區分配、同步物件、棧、程式計數器等準備好以後,就會建立一個作業系統原生執行緒。 Java 執行緒結束,原生執行緒隨之被回收。作業系統負責排程所有執行緒,並把它們分配到任何可 用的 CPU 上。當原生執行緒初始化完畢,就會呼叫 Java 執行緒的 run() 方法。當執行緒結束時,會釋放原生執行緒和 Java 執行緒的所有資源。

Hotspot JVM 後臺執行的系統執行緒主要有下面幾個:

2.2.JVM 記憶體區域

JVM 記憶體區域主要分為執行緒私有區域【程式計數器、虛擬機器棧、本地方法區】、執行緒共享區 域【JAVA 堆、方法區】、直接記憶體。 執行緒私有資料區域生命週期與執行緒相同, 依賴使用者執行緒的啟動/結束 而 建立/銷燬(在 Hotspot VM 內, 每個執行緒都與作業系統的本地執行緒直接對映, 因此這部分記憶體區域的存/否跟隨本地執行緒的 生/死對應)。執行緒共享區域隨虛擬機器的啟動/關閉而建立/銷燬。 直接記憶體並不是 JVM 執行時資料區的一部分, 但也會被頻繁的使用: 在 JDK 1.4 引入的 NIO 提 供了基於 Channel 與 Buffer 的 IO 方式, 它可以使用 Native 函式庫直接分配堆外記憶體, 然後使用 DirectByteBuffer 物件作為這塊記憶體的引用進行操作(詳見: Java I/O 擴充套件), 這樣就避免了在 Java 堆和 Native 堆中來回複製資料, 因此在一些場景中可以顯著提高效能。

2.2.1. 程式計數器(執行緒私有)

一塊較小的記憶體空間, 是當前執行緒所執行的位元組碼的行號指示器,每條執行緒都要有一個獨立的 程式計數器,這類記憶體也稱為“執行緒私有”的記憶體。 正在執行 java 方法的話,計數器記錄的是虛擬機器位元組碼指令的地址(當前指令的地址)。如 果還是 Native 方法,則為空。 這個記憶體區域是唯一一個在虛擬機器中沒有規定任何 OutOfMemoryError 情況的區域。

2.2.2. 虛擬機器棧(執行緒私有)

是描述java方法執行的記憶體模型,每個方法在執行的同時都會建立一個棧幀(Stack Frame) 用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫直至執行完成 的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。 棧幀( Frame)是用來儲存資料和部分過程結果的資料結構,同時也被用來處理動態連結 (Dynamic Linking)、 方法返回值和異常分派( Dispatch Exception)。棧幀隨著方法呼叫而建立,隨著方法結束而銷燬——無論方法是正常完成還是異常完成(丟擲了在方法內未被捕獲的異 常)都算作方法結束。

2.2.3. 本地方法區(執行緒私有)

本地方法區和 Java Stack 作用類似, 區別是虛擬機器棧為執行 Java 方法服務, 而本地方法棧則為 Native 方法服務, 如果一個 VM 實現使用 C-linkage 模型來支援 Native 呼叫, 那麼該棧將會是一個 C 棧,但 HotSpot VM 直接就把本地方法棧和虛擬機器棧合二為一。

2.2.4. 堆(Heap-執行緒共享)-執行時資料區

是被執行緒共享的一塊記憶體區域,建立的物件和陣列都儲存在 Java 堆記憶體中,也是垃圾收集器進行 垃圾收集的最重要的記憶體區域。由於現代 VM 採用分代收集演算法, 因此 Java 堆從 GC 的角度還可以 細分為: 新生代(Eden 區、From Survivor 區和 To Survivor 區)和老年代。

2.2.5. 方法區/永久代(執行緒共享)

即我們常說的永久代(Permanent Generation), 用於儲存被 JVM 載入的類資訊、常量、靜 態變數、即時編譯器編譯後的程式碼等資料. HotSpot VM把GC分代收集擴充套件至方法區, 即使用Java 堆的永久代來實現方法區, 這樣 HotSpot 的垃圾收集器就可以像管理 Java 堆一樣管理這部分記憶體, 而不必為方法區開發專門的記憶體管理器(永久帶的記憶體回收的主要目標是針對常量池的回收和型別 的解除安裝, 因此收益一般很小)。 執行時常量池(Runtime Constant Pool)是方法區的一部分。Class 檔案中除了有類的版 本、欄位、方法、介面等描述等資訊外,還有一項資訊是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加 載後存放到方法區的執行時常量池中。 Java 虛擬機器對 Class 檔案的每一部分(自然也包括常量 池)的格式都有嚴格的規定,每一個位元組用於儲存哪種資料都必須符合規範上的要求,這樣才會 被虛擬機器認可、裝載和執行。

2.3.JVM 執行時記憶體

Java 堆從 GC 的角度還可以細分為: 新生代(Eden 區From Survivor 區和 To Survivor 區)和老年代。

2.3.1. 新生代

是用來存放新生的物件。一般佔據堆的 1/3 空間。由於頻繁建立物件,所以新生代會頻繁觸發 MinorGC 進行垃圾回收。新生代又分為 Eden 區、ServivorFrom、ServivorTo 三個區。

2.3.1.1. Eden 區

Java 新物件的出生地(如果新建立的物件佔用記憶體很大,則直接分配到老 年代)。當 Eden 區記憶體不夠的時候就會觸發 MinorGC,對新生代區進行 一次垃圾回收。

2.3.1.2. ServivorFrom

上一次 GC 的倖存者,作為這一次 GC 的被掃描者。

2.3.1.3. ServivorTo

保留了一次 MinorGC 過程中的倖存者。

2.3.1.4. MinorGC 的過程(複製->清空->互換)

MinorGC 採用複製演算法。

1:eden、servicorFrom 複製到 ServicorTo,年齡+1

首先,把 Eden 和 ServivorFrom 區域中存活的物件複製到 ServicorTo 區域(如果有物件的年 齡以及達到了老年的標準,則賦值到老年代區),同時把這些物件的年齡+1(如果 ServicorTo 不夠位置了就放到老年區);

2:清空 eden、servicorFrom

然後,清空 Eden 和 ServicorFrom 中的物件;

3:ServicorTo 和 ServicorFrom 互換

最後,ServicorTo 和 ServicorFrom 互換,原 ServicorTo 成為下一次 GC 時的 ServicorFrom區。

2.3.2. 老年代

主要存放應用程式中生命週期長的記憶體物件。 老年代的物件比較穩定,所以 MajorGC 不會頻繁執行。在進行 MajorGC 前一般都先進行 了一次 MinorGC,使得有新生代的物件晉身入老年代,導致空間不夠用時才觸發。當無法找到足 夠大的連續空間分配給新建立的較大物件時也會提前觸發一次 MajorGC 進行垃圾回收騰出空間。 MajorGC 採用標記清除演算法:首先掃描一次所有老年代,標記出存活的物件,然後回收沒 有標記的物件。MajorGC 的耗時比較長,因為要掃描再回收。MajorGC 會產生記憶體碎片,為了減 少記憶體損耗,我們一般需要進行合併或者標記出來方便下次直接分配。當老年代也滿了裝不下的 時候,就會丟擲OOM(Out of Memory)異常。

2.3.3. 永久代

指記憶體的永久儲存區域,主要存放 Class 和 Meta(元資料)的資訊,Class 在被載入的時候被 放入永久區域,它和和存放例項的區域不同,GC 不會在主程式執行期對永久區域進行清理。所以這 也導致了永久代的區域會隨著載入的 Class 的增多而脹滿,最終丟擲 OOM 異常。

2.3.3.1. JAVA8 與元資料

在 Java8 中,永久代已經被移除,被一個稱為“元資料區”(元空間)的區域所取代。元空間 的本質和永久代類似,元空間與永久代之間最大的區別在於:元空間並不在虛擬機器中,而是使用 本地記憶體。因此,預設情況下,元空間的大小僅受本地記憶體限制。類的元資料放入 native memory, 字串池和類的靜態變數放入 java 堆中,這樣可以載入多少類的元資料就不再由 MaxPermSize 控制, 而由系統的實際可用空間來控制。

2.4.垃圾回收與演算法

2.4.1. 如何確定垃圾

2.4.1.1. 引用計數法

在 Java 中,引用和物件是有關聯的。如果要操作物件則必須用引用進行。因此,很顯然一個簡單 的辦法是通過引用計數來判斷一個物件是否可以回收。簡單說,即一個物件如果沒有任何與之關 聯的引用,即他們的引用計數都不為 0,則說明物件不太可能再被用到,那麼這個物件就是可回收 物件。

2.4.1.2. 可達性分析

為了解決引用計數法的迴圈引用問題,Java 使用了可達性分析的方法。通過一系列的“GC roots” 物件作為起點搜尋。如果在“GC roots”和一個物件之間沒有可達路徑,則稱該物件是不可達的。要注意的是,不可達物件不等價於可回收物件,不可達物件變為可回收物件至少要經過兩次標記 過程。兩次標記後仍然是可回收物件,則將面臨回收。

2.4.2. 標記清除演算法(Mark-Sweep)

最基礎的垃圾回收演算法,分為兩個階段,標註和清除。標記階段標記出所有需要回收的物件,清 除階段回收被標記的物件所佔用的空間。如圖

從圖中我們就可以發現,該演算法最大的問題是記憶體碎片化嚴重,後續可能發生大物件不能找到可 利用空間的問題。

2.4.3. 複製演算法(copying)

為了解決 Mark-Sweep 演算法記憶體碎片化的缺陷而被提出的演算法。按記憶體容量將記憶體劃分為等大小 的兩塊。每次只使用其中一塊,當這一塊記憶體滿後將尚存活的物件複製到另一塊上去,把已使用 的記憶體清掉,如圖:

這種演算法雖然實現簡單,記憶體效率高,不易產生碎片,但是最大的問題是可用記憶體被壓縮到了原 本的一半。且存活物件增多的話,Copying 演算法的效率會大大降低。

2.4.4. 標記整理演算法(Mark-Compact)

結合了以上兩個演算法,為了避免缺陷而提出。標記階段和 Mark-Sweep 演算法相同,標記後不是清 理物件,而是將存活物件移向記憶體的一端。然後清除端邊界外的物件。如圖:

2.4.5. 分代收集演算法

分代收集法是目前大部分 JVM 所採用的方法,其核心思想是根據物件存活的不同生命週期將記憶體 劃分為不同的域,一般情況下將 GC 堆劃分為老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特點是每次垃圾回收時只有少量物件需要被回收,新生代的特點是每次垃 圾回收時都有大量垃圾需要被回收,因此可以根據不同區域選擇不同的演算法。

2.4.5.1. 新生代與複製演算法

目前大部分 JVM 的 GC 對於新生代都採取 Copying 演算法,因為新生代中每次垃圾回收都要 回收大部分物件,即要複製的操作比較少,但通常並不是按照 1:1 來劃分新生代。一般將新生代 劃分為一塊較大的 Eden 空間和兩個較小的 Survivor 空間(From Space, To Space),每次使用 Eden 空間和其中的一塊 Survivor 空間,當進行回收時,將該兩塊空間中還存活的物件複製到另 一塊 Survivor 空間中。

2.4.5.2. 老年代與標記複製演算法

而老年代因為每次只回收少量物件,因而採用 Mark-Compact 演算法。

1. JAVA 虛擬機器提到過的處於方法區的永生代(Permanet Generation),它用來儲存 class 類, 常量,方法描述等。對永生代的回收主要包括廢棄常量和無用的類。

2. 物件的記憶體分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目 前存放物件的那一塊),少數情況會直接分配到老生代。

3. 當新生代的 Eden Space 和 From Space 空間不足時就會發生一次 GC,進行 GC 後,Eden Space 和 From Space 區的存活物件會被挪到 To Space,然後將 Eden Space 和 From Space 進行清理。

4. 如果 To Space 無法足夠儲存某個物件,則將這個物件儲存到老生代。

5. 在進行 GC 後,使用的便是 Eden Space 和 To Space 了,如此反覆迴圈。

6. 當物件在 Survivor 區躲過一次 GC 後,其年齡就會+1。預設情況下年齡到達 15 的物件會被

2.5.JAVA 四中引用型別

2.5.1. 強引用

在 Java 中最常見的就是強引用,把一個物件賦給一個引用變數,這個引用變數就是一個強引 用。當一個物件被強引用變數引用時,它處於可達狀態,它是不可能被垃圾回收機制回收的,即 使該物件以後永遠都不會被用到 JVM 也不會回收。因此強引用是造成 Java 記憶體洩漏的主要原因之 一。

2.5.2. 軟引用

軟引用需要用 SoftReference 類來實現,對於只有軟引用的物件來說,當系統記憶體足夠時它 不會被回收,當系統記憶體空間不足時它會被回收。軟引用通常用在對記憶體敏感的程式中。

2.5.3. 弱引用

弱引用需要用 WeakReference 類來實現,它比軟引用的生存期更短,對於只有弱引用的物件 來說,只要垃圾回收機制一執行,不管 JVM 的記憶體空間是否足夠,總會回收該物件佔用的記憶體。

2.5.4. 虛引用

虛引用需要 PhantomReference 類來實現,它不能單獨使用,必須和引用佇列聯合使用。虛 引用的主要作用是跟蹤物件被垃圾回收的狀態。

2.6.GC 分代收集演算法 VS 分割槽收集演算法

2.6.1. 分代收集演算法

當前主流 VM 垃圾收集都採用”分代收集”(Generational Collection)演算法, 這種演算法會根據 物件存活週期的不同將記憶體劃分為幾塊, 如 JVM 中的 新生代、老年代、永久代,這樣就可以根據 各年代特點分別採用最適當的 GC 演算法

2.6.1.1. 在新生代-複製演算法

每次垃圾收集都能發現大批物件已死, 只有少量存活. 因此選用複製演算法, 只需要付出少量 存活物件的複製成本就可以完成收集.

2.6.1.2. 在老年代-標記整理演算法

因為物件存活率高、沒有額外空間對它進行分配擔保, 就必須採用“標記—清理”或“標 記—整理”演算法來進行回收, 不必進行記憶體複製, 且直接騰出空閒記憶體.

2.6.2. 分割槽收集演算法

分割槽演算法則將整個堆空間劃分為連續的不同小區間, 每個小區間獨立使用, 獨立回收. 這樣做的 好處是可以控制一次回收多少個小區間 , 根據目標停頓時間, 每次合理地回收若干個小區間(而不是 整個堆), 從而減少一次 GC 所產生的停頓。

2.7.GC 垃圾收集器

Java 堆記憶體被劃分為新生代和年老代兩部分,新生代主要使用複製和標記-清除垃圾回收演算法; 年老代主要使用標記-整理垃圾回收演算法,因此 java 虛擬中針對新生代和年老代分別提供了多種不 同的垃圾收集器,JDK1.6 中 Sun HotSpot 虛擬機器的垃圾收集器如下:

2.7.1. Serial 垃圾收集器(單執行緒、複製演算法)

Serial(英文連續)是最基本垃圾收集器,使用複製演算法,曾經是JDK1.3.1 之前新生代唯一的垃圾 收集器。Serial 是一個單執行緒的收集器,它不但只會使用一個 CPU 或一條執行緒去完成垃圾收集工 作,並且在進行垃圾收集的同時,必須暫停其他所有的工作執行緒,直到垃圾收集結束。 Serial 垃圾收集器雖然在收集垃圾過程中需要暫停所有其他的工作執行緒,但是它簡單高效,對於限 定單個 CPU 環境來說,沒有執行緒互動的開銷,可以獲得最高的單執行緒垃圾收集效率,因此 Serial 垃圾收集器依然是 java 虛擬機器執行在 Client 模式下預設的新生代垃圾收集器。

2.7.2. ParNew 垃圾收集器(Serial+多執行緒)

ParNew 垃圾收集器其實是 Serial 收集器的多執行緒版本,也使用複製演算法,除了使用多執行緒進行垃 圾收集之外,其餘的行為和 Serial 收集器完全一樣,ParNew 垃圾收集器在垃圾收集過程中同樣也 要暫停所有其他的工作執行緒。13/04/2018 Page 32 of 283 ParNew 收集器預設開啟和 CPU 數目相同的執行緒數,可以通過-XX:ParallelGCThreads 引數來限 制垃圾收集器的執行緒數。【Parallel:平行的】 ParNew雖然是除了多執行緒外和Serial 收集器幾乎完全一樣,但是ParNew垃圾收集器是很多 java 虛擬機器執行在 Server 模式下新生代的預設垃圾收集器。

2.7.3. Parallel Scavenge 收集器(多執行緒複製演算法、高效)

Parallel Scavenge 收集器也是一個新生代垃圾收集器,同樣使用複製演算法,也是一個多執行緒的垃 圾收集器,它重點關注的是程式達到一個可控制的吞吐量(Thoughput,CPU 用於執行使用者程式碼 的時間/CPU 總消耗時間,即吞吐量=執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集時間)), 高吞吐量可以最高效率地利用 CPU 時間,儘快地完成程式的運算任務,主要適用於在後臺運算而 不需要太多互動的任務。自適應調節策略也是 ParallelScavenge 收集器與 ParNew 收集器的一個 重要區別。

2.7.4. Serial Old 收集器(單執行緒標記整理演算法 )

Serial Old 是 Serial 垃圾收集器年老代版本,它同樣是個單執行緒的收集器,使用標記-整理演算法, 這個收集器也主要是執行在 Client 預設的 java 虛擬機器預設的年老代垃圾收集器。 在 Server 模式下,主要有兩個用途:

1. 在 JDK1.5 之前版本中與新生代的 Parallel Scavenge 收集器搭配使用。

2. 作為年老代中使用 CMS 收集器的後備垃圾收集方案。

新生代 Serial 與年老代 Serial Old 搭配垃圾收集過程圖:

新生代 Parallel Scavenge 收集器與 ParNew 收集器工作原理類似,都是多執行緒的收集器,都使 用的是複製演算法,在垃圾收集過程中都需要暫停所有的工作執行緒。新生代 Parallel Scavenge/ParNew 與年老代 Serial Old 搭配垃圾收集過程圖:

2.7.5. Parallel Old 收集器(多執行緒標記整理演算法)

Parallel Old 收集器是Parallel Scavenge的年老代版本,使用多執行緒的標記-整理演算法,在 JDK1.6 才開始提供。 在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只 能保證新生代的吞吐量優先,無法保證整體的吞吐量,Parallel Old 正是為了在年老代同樣提供吞 吐量優先的垃圾收集器,如果系統對吞吐量要求比較高,可以優先考慮新生代 Parallel Scavenge 和年老代 Parallel Old 收集器的搭配策略。 新生代 Parallel Scavenge 和年老代 Parallel Old 收集器搭配執行過程圖:

2.7.6. CMS 收集器(多執行緒標記清除演算法)

Concurrent mark sweep(CMS)收集器是一種年老代垃圾收集器,其最主要目標是獲取最短垃圾 回收停頓時間,和其他年老代使用標記-整理演算法不同,它使用多執行緒的標記-清除演算法。 最短的垃圾收集停頓時間可以為互動比較高的程式提高使用者體驗。 CMS 工作機制相比其他的垃圾收集器來說更復雜,整個過程分為以下 4 個階段:

2.7.6.1. 初始標記

只是標記一下 GC Roots 能直接關聯的物件,速度很快,仍然需要暫停所有的工作執行緒。

2.7.6.2. 併發標記

進行 GC Roots 跟蹤的過程,和使用者執行緒一起工作,不需要暫停工作執行緒。

2.7.6.3. 重新標記

為了修正在併發標記期間,因使用者程式繼續執行而導致標記產生變動的那一部分物件的標記 記錄,仍然需要暫停所有的工作執行緒。

2.7.6.4. 併發清除

清除 GC Roots 不可達物件,和使用者執行緒一起工作,不需要暫停工作執行緒。由於耗時最長的並 發標記和併發清除過程中,垃圾收集執行緒可以和使用者現在一起併發工作,所以總體上來看 CMS 收集器的記憶體回收和使用者執行緒是一起併發地執行。 CMS 收集器工作過程:

2.7.7. G1 收集器

Garbage first 垃圾收集器是目前垃圾收集器理論發展的最前沿成果,相比與 CMS 收集器,G1 收 集器兩個最突出的改進是:

1. 基於標記-整理演算法,不產生記憶體碎片。

2. 可以非常精確控制停頓時間,在不犧牲吞吐量前提下,實現低停頓垃圾回收。

G1 收集器避免全區域垃圾收集,它把堆記憶體劃分為大小固定的幾個獨立區域,並且跟蹤這些區域 的垃圾收集進度,同時在後臺維護一個優先順序列表,每次根據所允許的收集時間,優先回收垃圾 最多的區域。區域劃分和優先順序區域回收機制,確保 G1 收集器可以在有限時間獲得最高的垃圾收 集效率。

2.8. JAVA IO/NIO

2.8.1. 阻塞 IO 模型

最傳統的一種 IO 模型,即在讀寫資料過程中會發生阻塞現象。當用戶執行緒發出 IO 請求之後,內 核會去檢視資料是否就緒,如果沒有就緒就會等待資料就緒,而使用者執行緒就會處於阻塞狀態,用 戶執行緒交出 CPU。當資料就緒之後,核心會將資料拷貝到使用者執行緒,並返回結果給使用者執行緒,使用者執行緒才解除 block 狀態。典型的阻塞 IO 模型的例子為:data = socket.read();如果資料沒有就 緒,就會一直阻塞在 read 方法。

2.8.2. 非阻塞 IO 模型

當用戶執行緒發起一個 read 操作後,並不需要等待,而是馬上就得到了一個結果。如果結果是一個 error 時,它就知道資料還沒有準備好,於是它可以再次傳送 read 操作。一旦核心中的資料準備 好了,並且又再次收到了使用者執行緒的請求,那麼它馬上就將資料拷貝到了使用者執行緒,然後返回。 所以事實上,在非阻塞 IO 模型中,使用者執行緒需要不斷地詢問核心資料是否就緒,也就說非阻塞 IO不會交出 CPU,而會一直佔用 CPU。典型的非阻塞 IO 模型一般如下:

但是對於非阻塞 IO 就有一個非常嚴重的問題,在 while 迴圈中需要不斷地去詢問核心資料是否就 緒,這樣會導致 CPU 佔用率非常高,因此一般情況下很少使用 while 迴圈這種方式來讀取資料。

2.8.3. 多路複用 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 模型來說,一旦事件響應體很大,那麼就會導致後續的事件 遲遲得不到處理,並且會影響新的事件輪詢。

2.8.4. 訊號驅動 IO 模型

在訊號驅動 IO 模型中,當用戶執行緒發起一個 IO 請求操作,會給對應的 socket 註冊一個訊號函 數,然後使用者執行緒會繼續執行,當核心資料就緒時會發送一個訊號給使用者執行緒,使用者執行緒接收到 訊號之後,便在訊號函式中呼叫 IO 讀寫操作來進行實際的 IO 請求操作。

2.8.5. 非同步 IO 模型

非同步 IO 模型才是最理想的 IO 模型,在非同步 IO 模型中,當用戶執行緒發起 read 操作之後,立刻就 可以開始去做其它的事。而另一方面,從核心的角度,當它受到一個 asynchronous read 之後, 它會立刻返回,說明 read 請求已經成功發起了,因此不會對使用者執行緒產生任何 block。然後,內 核會等待資料準備完成,然後將資料拷貝到使用者執行緒,當這一切都完成之後,核心會給使用者執行緒 傳送一個訊號,告訴它 read 操作完成了。也就說使用者執行緒完全不需要實際的整個 IO 操作是如何 進行的,只需要先發起一個請求,當接收核心返回的成功訊號時表示 IO 操作已經完成,可以直接 去使用資料了。

也就說在非同步 IO 模型中,IO 操作的兩個階段都不會阻塞使用者執行緒,這兩個階段都是由核心自動完 成,然後傳送一個訊號告知使用者執行緒操作已完成。使用者執行緒中不需要再次呼叫 IO 函式進行具體的 讀寫。這點是和訊號驅動模型有所不同的,在訊號驅動模型中,當用戶執行緒接收到訊號表示資料 已經就緒,然後需要使用者執行緒呼叫 IO 函式進行實際的讀寫操作;而在非同步 IO 模型中,收到訊號 表示 IO 操作已經完成,不需要再在使用者執行緒中呼叫 IO 函式進行實際的讀寫操作。

看到這裡還沒過癮,或者對以上技術點有任何疑問的,那麼就來群裡與更多的大佬交流切磋技術,戳這裡:咱們來一起抱團取暖,