深入底層瞭解Java併發機制系列之CPU快取模型
Javaer都知道,我們在編譯器上面編寫的Java程式碼經過編譯後會形成位元組碼,然後由類載入器載入到JVM中,JVM在執行位元組碼時,將它們轉換成一條條的彙編指令,最終由CPU的暫存器來執行,在CPU執行這些彙編的過程中需要讀取資料或者寫入資料,但CPU能讀取的資料只能來自計算機中的記憶體,隨著科技的發展,像Intel的部分CPU頻率特別是睿頻後已經到達了4.3GHZ了,但記憶體發展就比較緩慢,比如頂級的記憶體就3600MHZ左右,因此就造成了CPU的處理速度已經遠遠超過了記憶體的訪問速度,正常情況都是千倍的速度差距。
CPU快取模型
因為速度差距過大的原因,如果還是採用CPU直接讀取記憶體上面的資料,就會導致CPU資源嚴重的浪費!於是那些生產CPU的科技公司就設計出了,在CPU和記憶體之間增加一層快取的方案,剛才刻意到京東查了一下,現在的CPU基本都是三級快取了,L1 ,L2 ,L3 快取,我從百度圖片找來了兩張圖(如有侵權,請聯絡我,我馬上刪除)

CPU快取模型

CPU快取和記憶體訪問速度對比圖
通過這兩張圖,我們就可以更加直觀地感受到CPU快取和記憶體在訪問上面的速度的差距了,至於CPU核心的計算速度,和他們相比又是另一個級別的差距了。
那麼在有了CPU快取之後,我們就可以在程式執行的過程中,先從記憶體拷貝一份資料到CPU快取中,然後CPU計算都操作快取裡的資料,等執行完成的時候,再把快取中的資料更新到記憶體裡,從而增加CPU的使用效率。

CPU藉助快取和記憶體進行資料互動
在引入CPU快取之後,主了提高CPU的使用效率之外,還帶來了一個數據不一致的問題。比如i++這一個操作,在引入了CPU快取之後,他具體的情況是這樣的:
1:將記憶體中的i複製一份到CPU快取當中 2:CPU核心讀取CPU快取中的i 3:對i執行+1操作 4:將更新後的i寫回CPU快取 5:將CPU快取中的i更新到記憶體中
對於單執行緒來說,這完成不會有什麼問題,但是對於多執行緒來說,就會出現錯誤了,因為每個執行緒都有自己的工作空間。比如,現在有執行緒A和執行緒B同時對i執行i++操作,我們假設i一開始為0,我們期望最後的結果是2,但是最後的結果可能1:比如:
1:執行緒A將記憶體中的i複製一份到CPU快取當中,此時 i = 0; 2:執行緒B將記憶體中的i複製一份到CPU快取當中,此時 i = 0; 3:執行緒A對應的CPU核心1讀取CPU快取中的i,並執行+1操作,然後把更新後的i寫回CPU快取(i=1) 4:執行緒B對應的CPU核心2讀取CPU快取中的i,並執行+1操作,然後把更新後的i寫回CPU快取(i=1) 5:執行緒A將CPU快取中的i更新到記憶體(i=1) 6:執行緒B將CPU快取中的i更新到記憶體(i=1)
出現這種情況的原因也是很簡單的,比如多個CPU核心都從記憶體拷貝了一份資料到各自的快取當中,然後直接拿快取中的資料來執行+1操作,最後再把資料重新整理記憶體,於是就造成了這個問題。由於Demo過於簡單,我就不給出來了。下面我們回顧一下歷史,看看這個問題是怎麼被解決的,其實解決這個問題的方案有兩種:
第一種是早期的方案,因為CPU和計算機的其他元件通訊是通過匯流排來進行的, 比如資料通訊就是通過資料匯流排來進行,如果一個CPU核心要操作某個資料了, 就通過向匯流排傳送一個LOCK#的訊號來獲取匯流排鎖,那麼其他CPU核心就被阻塞了, 從而只有一個CPU核心能對記憶體進行訪問。
但是這種方案明顯效率是比較低的,於是就提出了第二方案:
通過快取一致性協議來解決資料不一致的問題,即CPU在操作CPU快取中的資料時, 如果發現它是一個共享變數(其他CPU也快取了一個副本),那麼他會進行以下的兩種操作: (1) 讀操作,只會將資料單純讀到暫存器,不做額外處理 (2) 寫操作,發出一個訊號告訴其他CPU核心,你快取的資料已經無效啦,讓其他CPU在讀取共享變數時,不得不重新去記憶體中重新拿過資料。
至此CPU快取模型我們已經介紹的差不多了,下一篇我們去了解Java記憶體模型,有了CPU快取模型和Java記憶體模型的知識,我們重新認識Java高併發又是另一種理解境界,下期見。

掃一掃,乾貨等著你來閱讀