今天主要就是介紹一下 CPU 中的多級緩存和亂序執行優化,為后面學習多線程做鋪墊。先來理解一下 CPU 的結構,后面再說 Java 虛擬機的內存模型。
先放兩張圖看一下 CPU 和各級緩存、內存、硬盤之間的關系。
下面就來介紹一下為什么會出現多級緩存,以及會出現什么問題,CPU 又是如何解決的。
為什么會出現多級緩存呢?說的簡單一點因為 CPU 的頻率太快了,而若是沒有緩存,直接讀取內存中的數據又太慢了,我們不想讓 CPU 停下來等待,所以加入了一層讀取速度大於內存但小於 CPU 的這么一層東西,這就是緩存。
加入緩存之后,CPU 需要數據就問緩存要,緩存沒有就從主存中讀取,並保留一份在緩存中。下次讀取就從緩存中讀取,加快速度。
但是,我們經常聽到 CPU 運行速度快,緩存次之,而內存慢一點,硬盤最慢。這又是為什么呢?
CPU 運行的快,那是因為一個時鍾周期短,一個時鍾周期是指機器碼0和1變化,實質就是電信號一高一低之間所用的時間,這可都是電信號啊,電的速度能不快嘛!也就 10 納秒左右吧,1 秒等於 10 的 9 次方納秒。
下面介紹一下緩存和內存
目前緩存基本上都是采用 SRAM 存儲器,SRAM 是英文 Static RAM 的縮寫,它是一種具有靜志存取功能的存儲器,不需要刷新電路即能保存它內部存儲的數據。不像 DRAM 內存那樣需要刷新電路,每隔一段時間,固定要對 DRAM 刷新充電一次,否則內部的數據即會消失,因此 SRAM 具有較高的性能,但是 SRAM 也有它的缺點,即它的集成度較低,相同容量的 DRAM 內存可以設計為較小的體積,但是 SRAM 卻需要很大的體積,這也是目前不能將緩存容量做得太大的重要原因。這中間也解釋了為什么內存中的數據一斷電就沒有了。
而 RAM(隨機讀寫存儲器)的工作原理大致是當 CPU 讀取主存時,將地址信號放到地址總線上傳給主存,主存讀到地址信號后,解析信號並定位到指定存儲單元,然后將此存儲單元數據放到數據總線上返回給 CPU。
磁盤慢就慢在它的讀取是需要借助於磁頭移動,這有尋址的過程,而這還是一個機械運動的過程,上面慢也是在和電信號打交道,而磁盤不僅需要電還需要擺臂,所以,最慢的就是它了。
再說一點,我們說的主存就是我們常說的內存,比方說我的電腦的內存是 12 G,磁盤的空間是 1 T。
整理一下就是,我們為了提高 CPU 的利用率,添加了多級緩存,但是呢,數據的讀取和保存都要在主存上進行,若是單線程是沒有問題的,一條路走下去,該讀讀該寫寫。
但是在多線程的情況下就會出現問題,因為每個線程都有自己的緩存,假如線程 1 從主存中讀取到 x,並對其加 1 ,此時還沒有寫回主存,線程 2 也從主存中讀取 x ,並加 1 ,它們是不知道對方的,也不可以讀取對方的緩存。這時都將 x 寫回主存,那此時 x 的值就少了 1 。
這也就是多線程情況下帶有緩存的問題,數據出現問題了,怎么辦呢?不能把緩存去掉吧,這時就需要一種協議,來保證不同的線程在讀寫主存數據時遵守某種規則以保證不會出現數據不一致的問題。這種協議有很多,其中用的比較多的是 MESI 協議,主要用來保證緩存的一致性。
MESI 為了保證多個緩存中共享數據的一致性,定義了 cache line 的四種狀態,而線程對 cache line 的四種操作可能會產生不一致的狀態,因此緩存控制器監聽到本地操作和遠程操作的時候,需要對地址一致的 cache line 狀態進行一致性修改,從而保證數據在多個緩存之間保持一致性。(M: modified E: Exclusive S: shared I: invalid)
CPU 中每個緩存行(caceh line)使用 4 種狀態進行標記(使用額外的兩位(bit)表示)。
M: 被修改(Modified)
該緩存行只被緩存在該 CPU 的緩存中,並且是被修改過的(dirty),即與主存中的數據不一致,該緩存行中的內存需要在未來的某個時間點(允許其它 CPU 讀取請主存中相應內存之前)寫回主存。當被寫回主存之后,該緩存行的狀態會變成獨享(exclusive)狀態。
E: 獨享的(Exclusive)
該緩存行只被緩存在該 CPU 的緩存中,它是未被修改過的(clean),與主存中數據一致。該狀態可以在任何時刻當有其它 CPU 讀取該內存時變成共享狀態(shared)。同樣地,當 CPU 修改該緩存行中內容時,該狀態可以變成 Modified 狀態。
S: 共享的(Shared)
該狀態意味着該緩存行可能被多個 CPU 緩存,並且各個緩存中的數據與主存數據一致(clean),當有一個 CPU 修改該緩存行中,其它 CPU 中該緩存行可以被作廢(變成無效狀態)。
I: 無效的(Invalid)
該緩存是無效的(可能有其它 CPU 修改了該緩存行)。
cache line 不同的狀態之間可以相互轉化,這也就是 MESI 協議的具體內容,比方說一個處於 M 狀態的緩存行必須時刻監聽所有試圖讀該緩存行相對應主存的操作,這種操作必須在緩存將該緩存行寫回主存並將狀態變成 S 狀態之前被延遲執行。
還有很多,就不一一說了。
再回頭看看那個圖是不是清晰多了,在 MESI 出現之前的解決緩存一致性的方案是總線鎖機制,這種解決方案效率很低,鎖住總線期間,其他 CPU 無法訪問內存。
說完了 CPU 中的多級緩存,再來看看 CPU 中的亂序執行優化,什么意思呢,處理器為提高運算速度而做出違背代碼原有順序的優化。雖然順序變了,但是執行的結果是不會變的。
舉個例子,我要去買杯飲料喝,正常邏輯是這樣的,去店里->點單->付錢->喝飲料。那其實我還可以在去店里的路上同時就把【點單付錢】一起搞定,又或者我先【點單】再去店里付錢拿飲料。
類比到 CPU 中也是這樣,求 x + y = ? 我計算 x 的值,再計算 y 的值,其實可以一起執行,或是先計算 y 的值,當然為了提高運算速度,它會同時計算 x 和 y 的值,一個 CPU 中又不是只有一個邏輯計算的單元。
亂序優化就是這樣,為了提高效率 CPU 做出的優化,這在單核的時候是不會有問題的,但是在多核時代又會出現問題。
再舉個例子,線程 1 需要借組一個 flag 變量執行一段邏輯,你能用 C線程 2 來亂序優化一下,先執行邏輯而不看 flag 變量的值?這是肯定不行的。
行還是不行肯定有一套規則,這套規則也就是內存屏障,太高級了聽不懂,換一種說法,就是說不同架構的處理器在其指令集中提供了不同的指令來發起內存屏障,對應在編程語言當中就是提供特殊的關鍵字來調用處理器相關的指令。結果就是有些指令能亂有些則不能!
好了,到這里就算把 CPU 級別的特性給說完了,這些知識對於我們理解 Java 內存模型(JMM) 以及多線程編程很有用。而 Java 內存模型就是借鑒了硬件的結構。
總結一下,在 CPU 中為了提高運行效率,加了多級緩存和亂序執行優化。加了多級緩存之后呢,會出現緩存不一致的情況,解決的辦法就是定義了 MESI 等類似協議。對於亂序執行優化帶來的問題,CPU 選擇內存屏障來解決,即定義了一套指令集,什么樣的指令不能執行亂序優化。
以上也就是介紹完了硬件的內存架構,看完了 CPU 中的騷操作,是不是迫不及待的想要看 Java 中的內存模型了,不要走開,下節更精彩!