當我們談 Java 併發的時候,你們在談什麼?

Java大蝸牛發表於2018-08-02

前言:

很多人在剛開始學 Java 的時候,會覺得多執行緒是一塊難啃的骨頭,特別是對於非科班的同學。究其原因,我想主要是因為沒有將多執行緒建立起一種模型,不清楚多執行緒的問題到底是怎麼產生的。在這裡,我就和大家聊一下我對Java 多執行緒的一些想法。

Java 是基於 Java 虛擬機器(JVM)實現的一套程式語言,我們寫的 Java 程式碼是要在 JVM 中才能執行。所謂虛擬機器,其實就是模擬了一個作業系統。一個常規作業系統所必備的功能,虛擬機器一般也會有。我們計算機的作業系統能管理記憶體資源,那麼虛擬機器當然也要能管理記憶體資源了。在 JVM 裡,從邏輯的角度來說,會把記憶體劃分為兩部分:  執行緒棧  和   。

嗯,我知道你們對這樣簡單粗暴的劃分方式有意見,JVM 裡面的記憶體劃分遠比上面說的複雜。

而我們今天的談論只涉及到  執行緒棧  (即虛擬機器棧)和   ,因此就簡單地認為 JVM 只劃分了這兩部分。

也就是說,JVM 裡面的記憶體模型,我們可以簡要地畫成下面的那樣:

當我們談 Java 併發的時候,你們在談什麼?

每一個執行緒對應一個執行緒棧,執行緒棧裡面的資源是私有的,也就是說我們線上程棧裡的變數(即所謂的區域性變數)是不會被多個執行緒共享。

堆記憶體是被所有執行緒所共享的,程式中建立的物件都會保留在堆記憶體中。

好了,說完了 Java 的記憶體模型,我們來看一看計算機的記憶體模型。

我們寫程式碼的時候,程式碼和資料一般都是儲存在硬碟中。當我們在 shell 中輸入完一個 javac命令,或者點選 IDE 的編譯按鈕的時候,我們的程式碼和資料會第一時間複製到記憶體中。複製完成之後會通知我們的 CPU 處理器,然後 CPU 開始執行命令,將記憶體中的資訊複製到 CPU 暫存器中,用來執行相應指令。在現代 CPU 中,CPU 暫存器執行速度非常快,而記憶體執行速度相對來說就非常慢了,為了彌補兩者執行速度之間的巨大差異,在記憶體和 CPU 暫存器之間會有快取記憶體(一般有三級快取),用來暫時存放從記憶體中獲取的資料。整個結構大體如下圖:

當我們談 Java 併發的時候,你們在談什麼?

上面這幅圖就是計算機的簡單儲存模型,這裡只畫了三層,第一層是 CPU 暫存器,第二層是 CPU 快取記憶體,第三層是記憶體。這裡的箭頭可以理解為資料匯流排,表示資料流動的方向。

在真實計算機中,CPU 快取記憶體一般有多級,其中一部分封裝在 CPU 核中,另一部分封裝在 CPU 處理器中(一個處理器可以有多個核),這裡為了方便,預設都封裝在 CPU 處理器中的。

如果 CPU 想要讀取我們程式碼中的資料,CPU 會先在快取記憶體中查詢需要的資料,如果找到了,那麼就直接使用這資料;如果在快取中沒有找到需要的資料,那麼就會繼續往下找,在記憶體中獲取資料,並且在快取中存放一份,再拿回 CPU 使用。

而 CPU 想要把處理後的資料寫回來的時候,就稍微麻煩一些了。如果 CPU 返回一個資料,就把該資料一級一級地往下送的話,那麼資料匯流排流量就會非常大。因此,什麼時間、以什麼樣的方式將返回的資料寫入下一級儲存器,以達到效能最優,是一個比較困難的問題。我們只知道, CPU 返回一個資料後,我們不會立即在記憶體中看到這個資料 。

瞭解了計算機的記憶體模型,這和 JVM 的記憶體模型有什麼關係呢?

我們已經知道,計算機的記憶體模型和 JVM 的記憶體模型是不一樣的,計算機的記憶體模型裡面並不區分執行緒棧和堆。而 JVM 裡的堆和執行緒棧資訊,一開始也只在計算機的記憶體中,只有當 CPU 執行指令需要堆或執行緒棧中的資訊時,JVM 裡面的一部分堆和執行緒棧的資料才會被載入到快取記憶體和 CPU 暫存器中。因此,JVM 的執行緒棧和堆的資訊可以用下面的圖來表示:

當我們談 Java 併發的時候,你們在談什麼?

也就是說,JVM 裡面的變數和物件,可能在計算機儲存結構中的任何地方存在。這就會導致兩個問題:

當執行緒更新一個共享變數的值時,會發生記憶體可見性問題(Memory Visibility)。

當多個執行緒對同一個變數進行更新操作時,會產生競態條件(Race Condition)。

這裡其實還可以思考一個問題,即在 JVM 裡面進行的執行緒操作,是如何分佈到作業系統的執行緒的。換句話說,JVM 裡面的執行緒是使用者態還是核心態?

其實 JVM 虛擬機器規範並未對此作出限制,不同的 JVM 可以有不同的實現。HotSpot 虛擬機器預設使用的是核心執行緒,也就是說 HotSpot 虛擬機器不干涉執行緒的排程,全權交由作業系統來處理。當然,如果想將執行緒繫結到特定的 CPU 核執行,也是可以的。HotSpot 虛擬機器中實現了 static bool bind_to_processor(uint processor_id); 方法,用來將執行緒繫結到指定的 CPU 核執行。

在此我向大家推薦一個Java高階群 : 725633148   裡面會分享一些資深架構師錄製的影片錄影:(有Spring,MyBatis,Netty原始碼分析,高併發、高效能、分散式、微服務架構的原理,JVM效能最佳化、分散式架構、面試資料)等這些成為架構師必備的知識體系 進群馬上免費領取,目前受益良多!

記憶體可見性

假設有一個共享物件,它最開始只是在記憶體中,當一個執行緒爭取到了左 CPU 的時間片,在這段時間裡將共享物件複製到左 CPU 的快取記憶體中,然後左 CPU 對這個共享物件做了一些修改並返回這個共享物件。之前我們說過, CPU 返回一個資料後,我們不會立即在記憶體中看到這個資料,因此,在共享物件的值返回到記憶體之前,如果右 CPU 也想使用這個共享物件,那麼右 CPU 拿到的共享物件不是左 CPU 修改後的共享物件,也就是說右 CPU 得到的共享物件的值不是最新的!

下面透過一副圖來說明這個問題:

當我們談 Java 併發的時候,你們在談什麼?

在上圖中,左邊的 CPU 會將記憶體中的 obj 物件複製一份在 CPU 快取記憶體中,然後 CPU 對其進行操作,修改了 obj 物件中 count 屬性的值,讓 obj.count 從 1 變成了 2。然而在 CPU 快取記憶體把 obj 最新的值返回到記憶體中之前,右邊的 CPU 執行了相同的程式碼,也從記憶體中獲取了 obj 物件,但它不知道左邊的 CPU 對 obj 物件進行修改了,它  看不見  obj 物件最新的值,因此,右邊的 CPU 獲取的 obj.count 的值還是 1 。

競態條件

可見性問題說的是一個執行緒對共享變數修改了之後,其他執行緒不能立即看到該共享變數最新的值得問題。如果有多個執行緒對同一個變數進行讀取和修改,那麼就可能發生競態條件。

當我們談 Java 併發的時候,你們在談什麼?

如上圖,假設左邊的 CPU 從記憶體中獲取了 obj 物件,並將其複製到 CPU 快取記憶體中,這個時候,右邊的 CPU 也從記憶體中獲取到了 obj 物件,也將其複製到了 CPU 快取記憶體中。然後兩個 CPU 都對 obj.count 的值增加 1。從整體上來看,obj.count 的值增加了兩次,而當左右兩邊的 CPU 快取記憶體將 obj 的值寫回到記憶體中時,會發現實際上 obj.count 的值只增加了 1 次。

下面的流程圖可以詳細說明這種情況:

當我們談 Java 併發的時候,你們在談什麼?

左 CPU 和右 CPU 同時爭奪 obj 物件的情況,就被成為“競態條件”。

在此我向大家推薦一個Java高階群 : 725633148   裡面會分享一些資深架構師錄製的影片錄影:(有Spring,MyBatis,Netty原始碼分析,高併發、高效能、分散式、微服務架構的原理,JVM效能最佳化、分散式架構、面試資料)等這些成為架構師必備的知識體系 進群馬上免費領取,目前受益良多!


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31545684/viewspace-2168995/,如需轉載,請註明出處,否則將追究法律責任。

相關文章