這篇簡單梳理下與多線程相關的硬件知識,了解它們能夠讓我們更清晰的了解多線程工作的本質,以及關鍵字synchronized
、volatile
、final
的實現原理。
我們會發現,每一個硬件部件的引入都是為了解決某些問題,然后它們又誕生了新的問題。(程序員就處在這樣的永無止境的循環中……)
高速緩存
1. 緩存概念
先來說說緩存的概念,現在的處理器運行速度遠大于內存的讀寫速度的,為了填補兩者之間鴻溝,硬件設計者引入了高速緩存的概念。
如左圖,高速緩存是一種存取速率遠比內存快而容量遠比主內存小的存儲部件。有了它之后,CPU不再與主內存直接打交道,而是讀、寫高速緩存里的數據,高速緩存再與主內存發生讀、寫。
如右圖,現代處理器一般具有多個層級的高速緩存,分為一級緩存(L1d Cache)、二級緩存(L2d Cache)、三級緩存(L3d Cache)。一級緩存可能直接集成在處理器內核中,存取速度 一級緩存 > 二級緩存 > 三級緩存。存儲容量一級緩存 < 二級緩存 < 三級緩存。
2. 緩存結構
高速緩存的內部結構是一個拉鏈散列表,和
HashMap
的底層結構以及原理(可見HashMap原理解析)十分相似。它分為若干桶,每個桶是一個鏈表,包含若干緩存條目(Cache Line
)
緩存條目近一步可以分為三個部分:
- Data Block(緩存行):存儲從主內存中讀取的數據以及準備寫入主內存的數據,一個緩存行可存儲多個變量
- Tag:包含緩存行中數據的位置信息
- Flag:緩存行的狀態信息
CPU訪問內存時,會通過內存地址解碼的三個數據:index
(桶編號)、tag
(緩存條目的相對編號)、offset
(變量在緩存條目中的位置偏移)來獲取高速緩存中對應的數據。
如果找到且緩存行的Flag為有效,則緩存命中;否則緩存會從主內存中加載對應數據,該過程處理器會處于停頓(stall)狀態不能執行其他指令。
3. 緩存一致性協議(MESI)
由于每個處理器都有自己的高速緩存,當多線程并發訪問同一個共享變量時,就會出現這些線程所在的處理器各自保存了一份該共享變量的副本數據。當一個處理器對副本數據進行更新后,如何讓其他處理器察覺并作出反應呢?這就要靠:緩存一致性協議(MESI協議)
注:下面兩節概念可快速瀏覽過,主要結合3.3節例子回過頭來理解
3.1 MESI概念
MESI協議將緩存條目的狀態劃分為了四種:
M:被修改(Modified)
該緩存行的數據是被修改過的,與主存中的數據不一致。任一時刻,多個處理器的高速緩存中,Tag值相同的緩存條目,只有一個能處于該狀態。
在其它CPU讀取同一Tag的緩存條目數據之前,該緩存行中的數據會寫回主存,然后變為獨享(exclusive)狀態E:獨享的(Exclusive)
該緩存行以獨占的方式保留了相應內存地址的副本數據,其他所有CPU上的高速緩存都不能保留該數據的有效副本。該緩存條目的數據與主存中數據一致。
在任何時刻當有其它CPU讀取該內存時,該狀態將變成共享狀態(shared);當CPU修改該緩存行中內容時,該狀態變成修改狀態(Modified)。S:共享的(Shared)
該緩存行的數據可能被多個CPU緩存,并且各個緩存中的數據與主存數據一致。
當有一個CPU修改該緩存行數據時,其它CPU中該緩存行可以被作廢,即變成無效狀態(Invalid)。I: 無效的(Invalid)
該緩存行是無效的,不包含任何內存地址對應的有效副本數據。該狀態是緩存條目的初始狀態。
3.2 MESI消息
另外,為了緩存之間的通訊,協調各個處理器的讀、寫內存操作,MESI協議還定義了下面的一組消息:
請求消息 | 描述 | 返回消息 | 描述 |
---|---|---|---|
Read | 通知其他處理器,主內存正準備讀取某個數據。該消息包含待讀取數據的內存地址 | Read Response | 返回請求讀取的數據,該消息可能是主內存返回的,也可能是其他高速緩存返回。 |
Invalidate | 通知其他處理器將高速緩存中指定內存地址對應的緩存條目狀態置為I ,即通知這些處理器刪除指定內存地址的副本數據 |
Invalidate Acknowledge | 接收Invalidate 消息必須回復該消息,表示已經刪除其高速緩存上面的數據 |
Read Invalidate | 由Read 和Invalidate 組合而成的復合消息。作用在于通知其他處理器,當前處理器準備更新(Read-Modify-Write)一個數據,請求其他處理器刪除自己高速緩存中的副本副本數據。 |
Read Response與Invalidate Acknowledge | 接收到請求的處理器必須返回這兩個消息 |
Writeback | 該消息包含需要寫入主內存的數據及其對應的內存地址 |
處理器在執行內存讀寫操作時,在有必要的情況下會往總線發送特定的請求消息,同時每個處理器還會嗅探(也稱攔截)總線中由其他處理器發出的請求消息并在一定條件下往總線回復相應的響應消息。
3.3 舉例說明
上面的概念簡單看一下就行,我們主要要明白的是:MESI協議對內存數據訪問的控制類似讀寫鎖,它使得針對同一地址的讀內存操作是并發的,而針對同一地址的寫內存操作是獨占的。從而保障了緩存間數據的一致
現在我們來舉兩個例子看一下具體過程:
并發讀
當處理器Processor 0要讀取緩存中的數據S時,如果發現S所在的緩存條目狀態為M、E或S,那么處理器可直接讀取數據。
如果S所在的緩存條目狀態狀態為 I,說明Processor 0的緩存中不包含S的有效數據。這時,Processor 0會往總線發送一條Read消息來讀取S的有效數據,而緩存狀態不為 I 的其他處理器(如Process 1)或主內存(其他處理器緩存條目狀都為 I 時從主內存讀)收到消息后需要回復Read Response,來將有效的S數據返回給發送者。
需要注意的是,返回有效數據的其他處理器(如Process 1),如果狀態為M,則會先將數據寫入主內存,此時狀態為E,然后在返回Read Response后,再將狀態更新為S。
這樣,Processor 0讀取的永遠是最新的數據,即使其他處理器對這個數據做了更改,也會獲取到其他處理器最新的修改信息。
互斥寫
當處理器Processor 0要向地址A中寫數據時,如果地址A所在的緩存條目狀態為E、M,說明Processor 0已擁有該數據的獨占權,Processor 0可直接將數據寫入A,然后將緩存條目狀態改為M
如果寫的緩存條目狀態為S,處理器Processor 0需要往總線發送Invalidate消息來獲取該緩存條目的獨占權,當接收到其他所有處理器返回的Invalidate Acknowledge消息后,Processor 0才會確定自己已獲得獨占權,然后再將數據更新到地址A中,并將對應的緩存條目狀態改為M
如果寫的緩存條目狀態為I,處理器Processor 0需要往總線發送Read Invalidate消息來獲取該緩存條目的獨占權,其他步驟同S
需要注意的是,如果接收到Invalidate消息的其他其他處理器,緩存條目狀態為M,則該處理器會先將數據寫入主內存(以方便發送Read Invalidate指令的處理器讀到最新值),然后再將狀態改為I
這樣,Processor 0與其他處理器寫的時候,永遠只有一個處理器能夠獲得獨占權,即實現了互斥寫。
寫緩沖器和無效化隊列
依照上面的MESI協議,多線程并發訪問同一個共享變量時,并發讀和互斥寫,應該是已經解決了數據一致性問題,那為什么我們編程中還是會出現 “可見性” 這樣線程不安全的問題呢?
原因在于寫緩沖器和無效化隊列的引入。MESI協議雖然解決了緩存一致性問題,但其本身有一個性能缺陷:處理器每次寫數據時,都得等待其他所有處理器將其高速緩存中對應的數據刪除,并接收到它們返回的Read Response與Invalidate Acknowledge消息后才執行寫操作。這個過程無疑是很消耗時間的。
無奈的硬件設計者,解決了緩存一致性問題后,為了解決新出現的性能問題,又引入了新的部件:寫緩沖器和無效化隊列。
1. 寫緩沖器
寫緩沖器是處理器內部一個容量比高速緩存還小的高速存儲部件,每個處理器都有自身的寫緩沖器,且一個處理器無法讀取另一個處理器上的寫緩沖器內容。寫緩沖器的引入主要是為了解決上面提到的MESI的寫延遲問題。
1.1 寫操作過程
引入寫緩沖器后,當處理器要寫入數據時:
如果相應的緩存條目狀態為 E、M,則直接寫入,無需發送消息(照舊)
如果相應的緩存條目狀態為 S, 處理器會將寫操作相關信息存入寫緩沖器,并發送Invalidate消息。(不再等待響應消息)
如果相應的緩存條目狀態為 I,發生“寫未命中”,將寫操作相關信息存入寫緩沖器,并發送Read Invalidate消息。(不再等待響應消息)
當處理器將寫操作寫入寫緩沖器后,則認為寫操作已經完成。而實際上,當處理器收到其他所有處理器回應的Read Response、Invalidate Acknowledge消息后,處理器才會將寫緩沖器中對應的寫操作寫入相應的緩存行,這個時候,寫操作才算真正完成。
寫緩沖器讓處理器在執行寫操作時不需要再額外的等待,減少了寫操作的延時,提高了處理器的指令執行效率。
1.2 讀操作過程
引入寫緩存器后,處理器讀取數據時,由于該數據的更新結果可能仍然停留在寫緩沖器中,所以處理器會先從寫緩沖器中找尋數據,沒有找到時,才從高速緩存中找。
這種處理器直接從寫緩沖器中讀取數據的技術被稱為:存儲轉發
2. 無效化隊列
處理器在接收到Invalidate消息后,并不馬上刪除消息中指定地址對應的副本數據,而是將消息存入無效化隊列之后就回復Invalidate Acknowledge消息,從而減少了執行寫操作的處理器的等待時間。
需要注意的是,有些處理器(如X86)可能并沒有使用無效化隊列
3. 寫緩沖器和無效化隊列帶來的新問題
寫緩沖器和無效化隊列的引入帶來了性能的提高,同時又帶來了新的兩個問題:內存重排序與可見性
3.1 內存重排序
Processor 1 | Processor 2 |
---|---|
data = 1; // S1 | |
ready = true; // S2 | |
while(!ready) continue; //L3 | |
system.out.println(data); //L4 |
如上表,變量data
初始值為0,變量ready
初始值為false。兩個處理器Processor 1和Processor 2在各自的線程上執行上述代碼。執行的絕對時間順序為 S1——>S2——>L3——>L4
以StoreStore(寫又寫)操作為例,看寫緩沖造成的重排序
如果S1步data值的寫操作被寫入寫緩沖器、還沒真正的寫到高速緩存中,而S2步的ready值的寫操作已經寫入到了高速緩存。那在L3步讀取ready值時,根據MESI協議,會讀到正確的ready值:true;但在L4步讀取data時,會讀到data的初始值0,而不是在另外一個處理器寫緩沖器中的值:修改值1
在處理器Processor 2看來,S1和S2的執行順序就好像反了一樣,即發生了重排序。以LoadLoad(讀又讀)為例,看無效化隊列造成的重排序
同上面的步驟,S2已被同步到高速緩存,S1寫入寫緩沖器,并發送了Invalidate消息。當執行L3時,讀取到正確的值:true,當執行到L4時,由于無效化隊列,Processor 2雖然發送了Invalidate Acknowledge消息,但并沒有刪除自己高速緩存中的data數據,所以會讀取到其高速緩存中的data:0
3.2 可見性
一個處理器的寫緩沖器中的內容是無法被其他處理器讀取的,這個也就造成了一個處理器更新一個共享變量后,對其他處理器而言,看不到這個更新的值,即可見性。
寫緩沖器是可見性問題的硬件根源。
內存屏障
為了解決寫緩沖器和無效化隊列帶來的可見性和重排序問題,操心碎的硬件設計者又推出了新的方案:內存屏障。
內存屏障是被插入兩個CPU指令之間的一種指令,用來禁止處理器指令發生重排序(像屏障一樣),從而保障有序性的。另外,為了達到屏障的效果,它也會使處理器寫入、讀取值之前,將寫緩沖器的值寫入高速緩存,清空無效隊列,從而“附帶”的保障了可見性。
內存屏障其實就是volatile
、synchronized
的底層實現原理,具體細節將在下一章討論。
本文總結自:
黃文海《Java多線程編程實戰指南》——第12章