JVM高級基礎__多線程可見性問題至CPU層面(入門篇)

一、介紹

研討會需要研討發言,因此準備如下草稿。內容探討volitle的可見性與及爲什麼多線程會存在併發問題。

(文章建立在羣體已經充分了解以下知識點)

------瞭解JVM的內存模型

------瞭解JVM如何加載一個java文件到內存

------瞭解線程是java程序運行的最小單位,JVM進程與線程之間的區別。

------瞭解volitle變量的用法

------瞭解CPU是一個用來解析執行指令的硬件,內部分爲寄存器,處理器[ 控制器,運算器,高速緩衝存儲器],數據傳輸的總線

二、分析

(依舊按照原來的流程做如下前提簡述)

1、什麼情況下會發生併發問題?

當多個線程同時訪問一份相同的數據時,將對爭奪該數據的使用權。這個時候將可能產生併發問題。

2、JVM與CPU之間的關係?

JVM是java運行的一個進程。而CPU是一個翻譯與及執行指令的機器。JVM的作用是向上提供服務功能,將class文件加載到方法區中,創建對象生成在內存中,向下調用系統的API來向CPU發送指令,讓CPU幹活。他們交互的空間就是內存。

3、描繪一個場景?

當多個線程同時競爭一個資源的時候,他們是各自在內存中複製了一份高速緩存,在高速緩存中修改後,再同步到內存中。

正常我們的數據在內存中也是一個物理標記,而當我們的數據同時被兩個線程盯上的時候就會產生併發問題了。


(以下正文)

       通常情況下,我們會使用【volitle實現變量可見性】,【同時禁止指令重排序】(這個概念是錯誤的)  來解析。我們分析線程會先複製一份到自己的【高速緩存空間】先在高速緩存空間修改完成後,再去判斷狀態是否和內存中的一致,如果一致執行同步。並讓其他線程監聽到內存中的數據已經被修改,自動失效高速緩存的數據。

      此處的分析依據是基於緩存一致性的MESI協議,即他規定每條緩存記錄都存在着一個狀態位,有如下四種變化:

此處解析:

我們知道CPU的組成中存在着高速緩衝存儲器,這一塊是屬於CPU內部的緩存,我們平時說的高速緩存就是指的這裏。通常情況下,CPU是通過將內存中的數據加載到高速緩存中,再經過CPU運算器進行運算。最終寫回內存。對於個CPU而言,高速緩衝存儲器裏面是具備很多塊高速緩存的。它的運行速度遠比jvm的運行速度要快得多,幾乎達到了納秒級別,正常情況下是不會對代碼產生影響的。

正常情況下的爭奪問題,CPU的高速緩存足以應對,其處理速度並不會導致緩存長時間讀取不到。【CPU緩存一致性

爲什麼會存在CPU的緩存呢?

無論併發線程到底有多快,在宏觀層面我們看到的可能是兩個線程同時併發操作,但實際上再微觀層面,也就是CPU底層他們是必須有個先來後到。我們都知道數據在內存中也就是一個物理標識位,那麼假設,如果不存在高速緩存塊不存在緩存一致性問題。那麼當兩個線程來搶奪同一塊數據的時候,總有一個先搶到,而搶不到的線程則只能阻塞等待【存在鎖機制】。這樣對CPU的性能無疑是一種浪費。因此CPU高速緩存塊其實是爲了提高CPU的性能而設置的不可或缺的一部分。同時也不會造成併發問題。

快速結論:正常數據進入CPU是先從內存中到CPU的高速緩存!


(既然如此那麼爲什麼java還是會出問題呢?)

問題出在JVM,也就是java在沒經過CPU允許的情況下,對CPU的緩存進行了利用。我們知道java是編譯解析型語言,其進入內存到被生成對象的過程是,JVM通過類加載器將.java文件讀取轉化爲class文件,經過一系列的加載驗證解析的操作後將class文件讀取到方法區中,在解析成對象後到內存中後,通過執行引擎進行數據處理。

執行引擎在這裏做了什麼操作呢? 主要負責垃圾回收,執行class文件,代碼優化。垃圾回收是我們常說的【GC】(此處不做討論),執行文件則是將方法區中的class二進制解析實例化爲對象的過程。而代碼優化則是執行引擎中另外一部分【即時編譯器


即時編譯器負責的工作主要是在不改變單指令的結果的情況下,將我們的代碼進行優化。此部分結合如下代碼分析:

說明:從以上可以看到我們的設計邏輯是這樣的:啓動一個線程,每次都去讀取布爾值flag,如果flag爲true那麼則加1,如果檢測到不爲ture那麼則停止線程內的加1循環。

咋一看似乎沒什麼問題,但實際上這段代碼是不會停下來的。因爲他被JIT編譯器進行了優化。在JIT編譯器中,正常我們思考的步驟是,while體每次循環都去讀取一次flag標誌。然後再執行判斷。而在JIT看來,既然每次都要去讀取flag標誌,而你在系統編譯中又將flag賦值爲true,那麼我爲什麼需要每次都去重複讀取呢?於是將 讀取的操作直接替換爲 已經被賦值的結果。

其替換思路如下:(Z相當於Flag,由於每次都需要讀取,因此被JIT給優化了)

 解析:從第一段代碼中得到結果,flag標誌需要每次都讀取用於判斷是否進入循環,但因爲JIT及時編譯器的優化,flag標誌變成了首次讀取後便不再變更的值,所以代碼最終也沒有按照我們的要求跑出循環。

結論:併發問題出現所見非所得問題可以則是由於JVM的JIT即時編譯器導致的。這個過程是JIT想着把值給緩存到高速緩存中而帶來的後果。


由於Java的JVM編譯與CPU硬件開發商之間在沒有經過任何協議的基礎上進行了利用緩存的操作,因此java社區【JCP】便制定了關於JSR規範,用於約束開發廠商對java的利用標準。Volitle就是基於JCP提出的JSR.133號規範。在規範中明確約束看對volitle的可見性是實現效果和要求。明確要求volitle修飾的變量不允許緩存。

基於JVM與CPU之間天然的關係,CPU同時也跟進更新,向JVM提供了內存屏障的兩個指令。【讀屏障,寫屏障】

寫屏障:當某個線程改變了某個被Volitle修飾的變量的時候,執行指令將最新的數據寫入到主內存中。讓其他線程可見

讀屏障:在執行讀指令之前,先執行一個讓高速緩存中的數據失效的指令,強制從主內存讀取新的數據。

(Volitle的可見性原理【爲什麼】)


所謂指令重排序? 

【volitle具有禁止指令重排序的能力】這句話本身是有問題的。JIT編譯器的指令重序是爲了優化代碼,是一種優化手段,volitle並沒有能力去阻止他不去指令重排序。volitle只能讓JVM去遵循某些規則。【程序的執行結果不能被改變,編譯器和處理器都必須遵循as-if-serial協議,也即是不會對存在數據依賴關係的操作做重排序】

結果:volitle聲明瞭不能被緩存,從而導致jvm在運行過程中需要遵循as-if-serial協議,不能改變程序原有的結果和語義,(必須實在地去拿數據,不能用緩存去替換實在數據)。

 

科普:什麼是數據依賴關係? 變量之間存在着數據傳遞的過程則存在數據依賴關係。比如:

A = 1;
B = 2;

----------<A與B之間不存在數據依賴關係,即使指令重排序也沒關係>
B = A + 1;
C = B ;
----------<A與B之間或第一條數據第二條數據之間存在着變量賦值,不允許被改變順序>

在上面的代碼中我們的flag做了一個操作:--------------------------->

(正常情況下:)
flag = true;
flag = false;//賦值
flag == true ?

(排序後:)
flag == true;
flag == true?
flag == false; //此代碼因爲上面持續執行進入循環,執行不到此處

注:上面的幾句存在着數據傳遞的過程因此不能被排序,否則會出錯。

 


結論:JAVA程序放在內存中運行,是基於CPU運行的,CPU是非常底層的處理器,提供了優化加速性能的高速緩存區域,本身沒有問題,但他提供的這個緩存被JIT即時編譯器進行了利用。JVM裏面自己的指令重排序做爲一種優化手段之一,本質上是爲了節約資源而設置,也沒有問題。CPU緩存和java的JIT運行的指令優化機制的混合使用最終才導致了多線程下出現所見非所得的問題。

 

根據可見性原理,以上面那段代碼爲例,如果我們要保證代碼的有效性。可以從如下三個方面進行操作。

1、將flag變量設置爲 由volite修改的可見性變量(實際上不可緩存變量,添加了內存屏障指令)

2、在JVM配置中將JVM設置爲禁止JIT即時編譯器啓用指令優化機制

3、在循環體中添加【Sychronized】鎖。

 

關於Sychronized爲什麼會生效?

1、jls8【java 語言編程規範8】中規定了關鍵字必須達到實現要求。jls8中明確指出,【sychronized得到獲取到鎖之前和鎖之後的所有數據變動】也就是說關鍵字必須獲得到鎖的實時數據,並根據實時數據進行判斷。此處可以理解在在獲取鎖之前執行了讀的內存屏障指令,從而保證了不會獲取到CPU緩存中的數據狀態。

2、需要注意的是,此處鎖描述的獲取鎖前後,並不包括獲取鎖中間。也即是說Sychronized在進入鎖之前和退出鎖時,獲取到的是實時性的設置了內存屏障的數據。而在鎖中間的數據則可能不是實時的。因此基於上面的代碼,如果使用sychronized,我們需要添加鎖的位置是在循環體的裏面。


總結:由於研討開會討論交流的很多文件內容都經過優化,不允許被公佈,此文章的目的應該着重於建立一個CPU內部數據流轉圖的意識圖。基於此意識圖幫助我們梳理多線程章程的分析和思考。我們得到?

------線程是一段代碼搬運流

------JVM是一個與CPU交互的API工具,通過調用操作系統提供的API向CPU發送指令

------volitle可見性問題本質上是對緩存的禁止導致了JIT繞過了指令排序

------CPU的內部數據形象

------JCP社區與JSLT文件是什麼?

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章