天天看點

JVM是如何進行多線程并行程式設計的

靈魂畫風小劇場鎮樓!

JVM是如何進行多線程并行程式設計的

我們在日常項目中會經會遇到希望用多線程來并行執行任務的場景。一方面,多線程可以提升執行效率,但同時它也增加了排查問題的難度。

其實,HotSpotVM裡就提供了管理多線程的架構,并通過這個架構來實作并行GC和并發GC。

學習HotSpotVM的設計,從中獲得靈感,能夠幫助我們更好地完成自己的項目。

今天,我們就來看看HotSpotVM是如何以多個線程并行執行任務的,然後再通過剖析源碼,看看并行GC具體的執行示例。

背景知識

Java語言自帶螢幕(monitor)這種同步結構。而HotSpotVM内部的互斥處理一般都會使用螢幕。

我們借助滑雪闆租賃商店的情景講解Java螢幕。

假設租賃商店中滑雪闆的尺寸和樣式都相同,而且商店非常窄小,一次隻能進入一位顧客。

如果前面已經有顧客進入商店,那麼其他顧客就隻能在店外排隊等待。當店内沒有顧客後,排在第一位的顧客就可以進入店内。進入店内的顧客可以租滑雪闆。如果沒有多餘的滑雪闆了,那麼顧客就要在店内的等候室裡等待。

返還滑雪闆的顧客同樣必須在店外排隊等待。将滑雪闆返還後,顧客可以呼叫一位在等候室裡等待的顧客或所有顧客。被呼叫的顧客隻有在店裡沒有其他顧客的情況下才可以進入店内。如果店外有人排隊,那麼他必須排到隊尾等待。

如果再次進入商店時滑雪闆恰巧又沒有了,那麼他還是必須進入等候室等待。

JVM是如何進行多線程并行程式設計的

以上是關于螢幕的比喻。這時,共享資源是滑雪闆,螢幕是租賃商店。如果将顧客看作線程,那麼同時隻能有一個線程進入螢幕。當租賃商店中有顧客時,商店處于被加鎖的狀态。顧客離開後商店被解鎖,其他顧客就可以進入商店了。就Java語言而言,在等候室内等待就是wait方法,通知等候室内的一位顧客就是notify方法,通知所有顧客就是notifyAll方法。

1. 并行執行的流程

HotSpotVM中多線程并行執行的機制主要由以下角色來完成。

●AbstractWorkGang:勞工集合

●AbstractGangTask:讓勞工執行的任務

●GangWorker:執行指定任務的勞工

這些角色并行執行的流程如下。

首先,如圖1所示,AbstractWorkGang隻有一個螢幕,它會讓屬于AbstractWorkGang的GangWorker在螢幕的等候室中等待。

圖1 步驟①

JVM是如何進行多線程并行程式設計的

(AbstractWorkGang隻有一個螢幕,它會讓GangWorker在等候室中等待)

由螢幕負責進行互斥處理的共享資源是任務資訊的布告闆。布告闆上有以下資訊。

●任務的位址

●任務的編号

●執行任務的勞工總數

●完成任務的勞工總數

接下來,客戶會在布告闆上寫下希望并發執行的任務的資訊(圖2)。

圖2 步驟②

JVM是如何進行多線程并行程式設計的

(客戶擷取螢幕的鎖并在布告闆上寫下任務資訊)

客戶帶來的實際任務可以是繼承自AbstractGangTask類的任何執行個體。

客戶會在布告闆上寫下該執行個體的位址作為任務的位址,而任務的編号則是上一次任務的編号加1。

在本例中,這個值是1。執行任務的勞工總數與完成任務的勞工總數分别被初始化為0

接下來,客戶會通知所有正在等待的勞工,然後自己進入等候室(圖3)。 

圖3 步驟③

JVM是如何進行多線程并行程式設計的

(勞工們一個接一個地進入螢幕,将布告闆上的資訊記在自己的筆記本上,然後離開螢幕。)

被通知到的勞工們一個接一個地進入螢幕,确認布告闆上的資訊。

勞工會記錄自己上次執行過的任務編号,如果布告闆上的編号與記錄的編号相同,那麼為了避免重複執行任務,他們會忽略這個任務并進入等候室等待。

如果是新的任務編号,那麼他們會在筆記本上記錄下布告闆上的資訊(任務的位址和編号),并将布告闆上執行任務的勞工總數加1,然後離開螢幕去執行任務。

執行完任務後,勞工會再次進入螢幕。這時,為了告訴大家自己完成了一項任務,他會将布告闆上“完成任務的勞工總數”加1(圖4)。 

圖4 步驟④

JVM是如何進行多線程并行程式設計的

(在任務完成後,勞工再次進入螢幕,更新布告闆上的資訊并進入等候室等待。)

接着,這個勞工會将等候室中的所有人(包括客戶)都叫出來,然後自己進入等候室。

所有勞工的任務都執行完成後,執行任務的勞工總數應當與完成任務的勞工總數相同。 

客戶進入螢幕後會确認布告闆上的資訊,看看是否所有的任務都完成了(圖5)。

圖5 步驟⑤

JVM是如何進行多線程并行程式設計的

(客戶進入螢幕,在看到所有勞工都執行完任務後退出螢幕)

如果還有尚未完成的任務,那麼客戶就會在等候室裡等待勞工完成任務。所有任務都完成之後,客戶才會滿意地離開螢幕。

以上就是并行執行的流程。 

JVM是如何進行多線程并行程式設計的

1.1  AbstractWorkGang類

接下來我們詳細地講解一下并行執行流程中的出場角色。AbstractWorkGang類的繼承關系如圖6所示。 

圖6 AbstractWorkGang類的繼承關系

JVM是如何進行多線程并行程式設計的

AbstractWorkGang類中定義了WorkGang所需的接口。

JVM是如何進行多線程并行程式設計的

第127行代碼定義的虛函數run_task()負責将任務交給worker并讓它們執行任務。run_task()的實體是在子類 WorkGang類中定義的。 

第139行至第156行代碼定義了WorkGang所需的屬性。這部分相當于1.中講過的“任務資訊的布告闆”中的資料。

圖6中展示的FlexibleWorkGang類能夠在之後靈活(flexible)地改變可以執行任務的勞工數量。并行GC會經常用到這個類。

1.2 AbstractGangTask類

AbstractGangTask類的繼承關系如圖7所示。

圖7 AbstractGangTask類的繼承關系

JVM是如何進行多線程并行程式設計的

AbstractGangTask類定義了并行執行任務所需的接口。 

JVM是如何進行多線程并行程式設計的

其中最重要的成員函數就是第68行代碼所定義的work()。work()是負責執行任務的函數,它接收勞工的編号作為參數。 

任務的詳細處理是在G1ParTask等子類的work()方法中定義的。客戶将AbstractGangTask的子類的執行個體傳遞給AbstractWorkGang,然後讓他們并行執行任務。

1.3 GangWorker類

GangWorker類是負責實際執行任務的類,它的一個祖先類是Thread類(圖8)。

圖8 GangWorker類的繼承關系

JVM是如何進行多線程并行程式設計的

由于一個GangWoker的執行個體對應一個線程,是以GangWoker也被稱為勞工線程。 

JVM是如何進行多線程并行程式設計的

GangWorker類中定義有一個成員變量_gang,其中存放着自身所屬的AbstractWorkGang。

JVM是如何進行多線程并行程式設計的

2.

 并行GC的執行示例

下面,我們來一邊閱讀實際代碼,一邊回顧上一節中的内容。

代碼清單1.1展示了作為客戶的主線程執行并行GC的示例代碼。

代碼清單1.1 并行GC的示例代碼

JVM是如何進行多線程并行程式設計的
JVM是如何進行多線程并行程式設計的

2.1  ①準備勞工

首先,通過代碼清單1.1的①中所示部分建立和初始化FlexibleWorkGang的執行個體,使之變為前面出現過的圖1的狀态。

建立和初始化FlexibleWorkGang的時序圖如圖9所示。

圖9 建立和初始化WorkGang的時序圖

JVM是如何進行多線程并行程式設計的

讓我們從上往下看一看這個流程。首先是AbstractWorkGang的構造函數。

JVM是如何進行多線程并行程式設計的

上面是初始化螢幕和資料的代碼,大家隻看懂這一點即可。其他代碼沒有太多關系,可以忽略。

在建立出AbstractWorkGang類的執行個體後,要通過成員函數initialize_workers()初始化勞工。

JVM是如何進行多線程并行程式設計的

第81行代碼用來按照客戶希望的勞工數量建立一個勞工數組,第92行至第103行代碼則用來建立勞工。

第93行代碼是調用allocate_worker()建立GangWorker,而第96行代碼和第101行代碼分别是建立勞工線程和讓勞工線程開始執行處理。

第93行代碼中的allocate_worker()的源碼如下。

JVM是如何進行多線程并行程式設計的

allocate_worker()函數以this(自己所屬的AbstractWorkerGang)和勞工的編号為參數建立GangWorker的執行個體。

initialize_workers()是通過内部調用os::start_thread()來讓線程開始執行處理。由于GangWorker繼承自Thread類,是以os::start_thread()實際上會調用讓線程開始執行處理的run()函數。

讓我們看一看GangWorker類的run()函數。

JVM是如何進行多線程并行程式設計的

run()函數調用了loop()函數。這裡我們隻看loop()函數中進入螢幕等候室等待的部分。 

JVM是如何進行多線程并行程式設計的

首先在第243行擷取自己所屬的AbstractWorkGang的螢幕。 

在第249行給螢幕加鎖并進入螢幕。 

然後,在第268行中的循環開始處檢查是否有任務。 

由于線程啟動時多數情況下是沒有任務的,是以這時基本上都會執行第284行代碼調用wait()。

2.2 ②建立任務

準備好勞工後,接下來要建立讓勞工執行的任務。請參考代碼清單1.1中②的部分。這裡以繼承自AbstractGangTask的G1GC标記任務CMConcurrentMarkingTask為例進行講解。

JVM是如何進行多線程并行程式設計的

在第1155行至第1157行定義的CMConcurrentMarkingTask的構造函數接收用來執行work()的變量作為參數。由于work()的參數是确定的,是以任務類的執行個體必須将執行各個任務時所需的資訊作為成員變量儲存起來。

第1095行至第1153行代碼是CMConcurrentMarkingTask要執行的任務的内容。建立出的各個GangWorker會調用這個work()方法。 

2.3 ③并行執行任務

最後是将任務交給勞工。

代碼清單1.1中③的部分會調用FlexibleWorkGang的run_task()。

JVM是如何進行多線程并行程式設計的

以任務為參數的run_task()首先會通過第132行代碼擷取螢幕的鎖。

然後,在第139行寫好任務資訊,在第140行至第142行更新其他資訊。 

這一部分與圖2相對應。

JVM是如何進行多線程并行程式設計的

然後,在第144行通知在等候室中等待的勞工。

第146行至第153行的while循環的退出條件是“所有的勞工都完成任務”。 

如果不滿足條件,那麼在第152行的客戶會繼續等待。這一部分與圖5相對應。 

各個勞工在GangWorker的loop()函數中調用wait(),等待被給予可以執行的任務。 

下面我們稍微詳細地看一看loop()。

JVM是如何進行多線程并行程式設計的

顧名思義,第242行的previous_sequence_number是用來記錄上一個任務編号的局部變量。

從第244行開始的for循環每循環一次,勞工就會執行一個任務。 

第245行代碼中的WorkData是記錄WorkerGang中任務資訊(布告闆資訊)的局部變量。

此外,第246行代碼中的part是記錄勞工順序的局部變量。這些局部變量都是在執行任務的循環的作用域(scope)中定義的,是以執行任務的循環每循環一次,它們就會被清空一次。

從第268行開始的for循環是從WorkerGang擷取任務的循環。 

通常勞工是在第284行處于等待狀态,直到接收到notify_all()的通知才會開始工作。

勞工開始工作後,第285行代碼中的internal_worker_poll()會将任務資訊複制到局部變量中。

在擷取了這些資訊後,第276行和第277行的條件分支代碼會檢查目前是否有應該執行的任務。如果有,則在第278行将自己已經啟動的資訊記錄到GangWorker中,然後在第279行調用notify_all(),将勞工的順序儲存在part中并退出循環。請注意,這裡在退出循環的同時還解除了螢幕的鎖。 

然後,第308行以part為參數調用了任務的work()函數。這裡會實際地執行任務。

到目前為止的這一部分與圖3相對應。

JVM是如何進行多線程并行程式設計的

任務完成後,勞工會再次擷取鎖,并将任務完成的資訊寫入到GangWorker中。

接下來,勞工會調用notify_all(),将完成的任務的編号複制到previous_sequence_number中,然後傳回到for循環的開始處。 

這一部分與圖4相對應。 

到此,勞工就完成了一個任務。當所有的勞工都執行完任務後,客戶會檢查GangWorker中的資訊,确認所有任務全部完成。這樣run_task()函數的執行也就結束了。

以上就是HotSpotVM中多線程并行執行GC任務的流程和源碼實作。 

那麼,HotSpotVM又是如何控制線程,與mutator并發執行GC的?

并發GC由哪些類實作?各類有什麼作用,繼承關系又如何?

HotSpotVM的“安全點”和“VM線程”又是什麼意思呢?

欲知後事如何,沒有下回分解!

一切盡在此書中!

JVM是如何進行多線程并行程式設計的

推 薦 閱 讀

深入JVM,這本書講透了G1回收的原理和實作!

圖 靈 社 群

JVM是如何進行多線程并行程式設計的
JVM是如何進行多線程并行程式設計的

點個「贊」或「在看」嘛!

JVM是如何進行多線程并行程式設計的