當咱們談 Java 併發的時候,大家在談什麼?

前言:java

不少人在剛開始學 Java 的時候,會以爲多線程是一塊難啃的骨頭,特別是對於非科班的同窗。究其緣由,我想主要是由於沒有將多線程創建起一種模型,不清楚多線程的問題究竟是怎麼產生的。在這裏,我就和你們聊一下我對Java 多線程的一些想法。面試

Java 是基於 Java 虛擬機(JVM)實現的一套編程語言,咱們寫的 Java 代碼是要在 JVM 中才能運行。所謂虛擬機,其實就是模擬了一個操做系統。一個常規操做系統所必備的功能,虛擬機通常也會有。咱們計算機的操做系統能管理內存資源,那麼虛擬機固然也要能管理內存資源了。在 JVM 裏,從邏輯的角度來講,會把內存劃分爲兩部分: 線程棧 和  。shell

嗯,我知道大家對這樣簡單粗暴的劃分方式有意見,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 裏面的變量和對象,可能在計算機存儲結構中的任何地方存在。這就會致使兩個問題:

  1. 當線程更新一個共享變量的值時,會發生內存可見性問題(Memory Visibility)。
  2. 當多個線程對同一個變量進行更新操做時,會產生競態條件(Race Condition)。

這裏其實還能夠思考一個問題,即在 JVM 裏面進行的線程操做,是如何分佈到操做系統的線程的。換句話說,JVM 裏面的線程是用戶態仍是內核態?

其實 JVM 虛擬機規範並未對此做出限制,不一樣的 JVM 能夠有不一樣的實現。HotSpot 虛擬機默認使用的是內核線程,也就是說 HotSpot 虛擬機不干涉線程的調度,全權交由操做系統來處理。固然,若是想將線程綁定到特定的 CPU 核執行,也是能夠的。HotSpot 虛擬機中實現了 static bool bind_to_processor(uint processor_id); 方法,用來將線程綁定到指定的 CPU 核運行。

內存可見性

假設有一個共享對象,它最開始只是在內存中,當一個線程爭取到了左 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高級羣 :725633148 裏面會分享一些資深架構師錄製的視頻錄像:(有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構、面試資料)等這些成爲架構師必備的知識體系 進羣立刻免費領取,目前受益良多!

競態條件

可見性問題說的是一個線程對共享變量修改了以後,其餘線程不能當即看到該共享變量最新的值得問題。若是有多個線程對同一個變量進行讀取和修改,那麼就可能發生競態條件。

當咱們談 Java 併發的時候,大家在談什麼?

 

如上圖,假設左邊的 CPU 從內存中獲取了 obj 對象,並將其複製到 CPU 高速緩存中,這個時候,右邊的 CPU 也從內存中獲取到了 obj 對象,也將其複製到了 CPU 高速緩存中。而後兩個 CPU 都對 obj.count 的值增長 1。從總體上來看,obj.count 的值增長了兩次,而當左右兩邊的 CPU 高速緩存將 obj 的值寫回到內存中時,會發現實際上 obj.count 的值只增長了 1 次。

下面的流程圖能夠詳細說明這種狀況:

當咱們談 Java 併發的時候,大家在談什麼?

 

左 CPU 和右 CPU 同時爭奪 obj 對象的狀況,就被成爲「競態條件」。

相關文章
相關標籤/搜索