自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

并發(fā)場景下的底層細(xì)節(jié) - 偽共享問題

系統(tǒng) 其他OS
眾所周知,為了緩解內(nèi)存和 CPU 之間速度不匹配的矛盾,引入了高速緩存這個東西,它的容量比內(nèi)存小很多,但是交換速度卻比內(nèi)存要快得多。

最近看書看到的偽共享問題,直接觸碰到知識盲區(qū)了,之前確實沒聽說過這個東西,打開百度就像吃飯一樣自然。

雖然面經(jīng)上出現(xiàn)的次數(shù)不多,不過我覺得還是很重要的一個問題,而且不難,花個五分鐘就能弄清楚~

老規(guī)矩,背誦版在文末。點擊閱讀原文可以直達(dá)我收錄整理的各大廠面試真題

三級緩存架構(gòu)

眾所周知,為了緩解內(nèi)存和 CPU 之間速度不匹配的矛盾,引入了高速緩存這個東西,它的容量比內(nèi)存小很多,但是交換速度卻比內(nèi)存要快得多。

之前我們畫過這樣的分級存儲體系結(jié)構(gòu):

?

事實上,高速緩存仍然存在細(xì)分,也稱為三級緩存結(jié)構(gòu):一級(L1)緩存、二級(L2)緩存、三級(L3)緩存

越靠近 CPU 的緩存,速度越快,容量也越小。所以 L1 緩存容量最小但是速度最快;L3 緩存容量最大同時速度也最慢

當(dāng) CPU 執(zhí)行運算的時候,它會先去 L1 緩存查找所需的數(shù)據(jù)、如果沒有找到的話就再去 L2 緩存、然后是 L3 緩存,如果最后這三級緩存中都沒有命中,那么 CPU 就要去訪問內(nèi)存了。

顯然,CPU 走得越遠(yuǎn),運算耗費的時間就越長。所以盡量確保數(shù)據(jù)存在 L1 緩存中能夠提升大計算量情況下的運行速度。

需要注意的是,CPU 和三級緩存以及內(nèi)存的對應(yīng)使用關(guān)系:

  • L1 和 L2 都是只能被一個單獨的 CPU 核心使用
  • L3 可以被單個插槽上的所有 CPU 核心共享
  • 內(nèi)存由全部插槽上的所有 CPU 核心共享

如下圖所示:

另外,這三級緩存空間中的數(shù)據(jù)是如何組織起來的呢?換句話說,數(shù)據(jù)在這三級緩存中的存儲形式是什么樣的呢?

Cache Line(緩存行)!

緩存中的基本存儲單元就是 Cache Line。

每個 Cache Line 通常是 64 字節(jié),也就是說,一個 Java 的 long 類型變量是 8 字節(jié),一個 Cache Line 中可以存 8 個 long 類型的變量。

所以小伙伴們看出來了嗎~ 緩存中的數(shù)據(jù)并不是按照一個一個單獨的變量這樣存儲組織起來的,而是多個變量會放到一行中。

偽共享問題 False Sharing

說了這么多似乎還并未觸及偽共享問題,別急,我們離真相已經(jīng)很近~

在程序運行的過程中,由于緩存的基本單元 Cache Line 是 64 字節(jié),所以緩存每次更新都會從內(nèi)存中加載連續(xù)的 64 個字節(jié)。

如果訪問的是一個 long 類型數(shù)組的話,當(dāng)數(shù)組中的一個值比如 v1 被加載到緩存中時,接下來地址相鄰的 7 個元素也會被加載到緩存中。(這也能解釋為啥我們數(shù)組總是能夠這么快,像鏈表這種離散存儲的數(shù)據(jù)結(jié)構(gòu),就無法享受到這種紅利)。

But,這波紅利很可能帶來無妄之災(zāi)。

舉個例子,如果我們定義了兩個 long 類型的變量 a 和 b,他們在內(nèi)存中的地址是緊挨著的,會出現(xiàn)什么問題?

如果我們想要訪問 a 的話,那么 b 也會被存儲到緩存中來。

懵了懵了,這有什么問題嗎,看起來似乎沒有什么毛病,多么 nice 的特性啊

來來來,直接上個例子

回想下上文提到的 CPU 和三級緩存以及內(nèi)存的對應(yīng)使用關(guān)系,設(shè)想這種情況,如果一個 CPU 核心的線程 T1 在對 a 進(jìn)行修改,另一個 CPU 核心的線程 T2 卻在對 b 進(jìn)行讀取。

當(dāng) T1 修改 a 的時候,除了把 a 加載到 Cache Line,還會享受一波紅利,把 b 同時也加載到 T1 所處 CPU 核心的 Cache Line 中來,對吧。

根據(jù) MESI 緩存一致性協(xié)議,修改完 a 后這個 Cache Line 的狀態(tài)就是 M(Modify,已修改),而其它所有包含 a 的 Cache Line 中的 a 就都不是最新值了,所以都將變?yōu)?I 狀態(tài)(Invalid,無效狀態(tài))

這樣,當(dāng) T2 來讀取 b 時,誒,發(fā)現(xiàn)他所處的 CPU 核心對應(yīng)的這個 Cache Line 已經(jīng)失效了,mmp,它就需要花費比較長的時間從內(nèi)存中重新加載了。

問題已經(jīng)顯而易見了,b 和 a 沒有任何關(guān)系,每次卻要因為 a 的更新導(dǎo)致他需要從內(nèi)存中重新讀取,拖慢了速度。這就是偽共享

表面上 a 和 b 都是被獨立線程操作的,而且兩操作之間也沒有任何關(guān)系。只不過它們共享了一個緩存行,但所有競爭沖突都是來源于共享。

用更書面的解釋來定義偽共享:當(dāng)多線程修改互相獨立的變量時,如果這些變量共享同一個緩存行,就會無意中影響彼此的性能,導(dǎo)致無法充分利用緩存行特性,這就是偽共享。

偽共享解決方案

我們先來舉個例子看看一段偽共享代碼的耗時,如下所示,我們定義一個 Test 類,它包含兩個 long 的變量,分別用兩個線程對這兩個變量進(jìn)行自增 1 億次,這段代碼耗時 5625ms

對于偽共享,一般有兩種方法,其實思想都是一樣的:

1)padding:就是增大數(shù)組元素之間的間隔,使得不同線程存取的元素位于不同的緩存行上,以空間換時間

上面提到過,一個 64 字節(jié)的 Cache Line 中可以存 8 個 long 類型的變量,我們在 a 和 b 這兩個 long 類型的變量之間再加 7 個 long 類型,使得 a 和 b 處在不同的 Cache Line 上面:

class Pointer {
volatile long a;
long v1, v2, v3, v4, v5, v6, v7;
volatile long b;
}

再次運行程序,會發(fā)現(xiàn)輸出時間神奇的縮短為了 955ms

2)JDK1.8 提供了 @Contended 注解:就是把我們手動 padding 的操作封裝到這個注解里面了,這個注解可以放在類上也可以放在字段上,這里就不多做說明了

class Test {
@Contended
volatile long a; // 填充 a
volatile long b;
}

需要注意的是,默認(rèn)使用這個注解是無效的,需要在 JVM 啟動參數(shù)加上 XX:-RestrictContended 才會生效

最后放上這道題的背誦版:

 面試官:講一下偽共享問題

小牛肉:偽共享問題其實是由于高速緩存的特性引起的,三級高速緩存中的數(shù)據(jù)并不是一個變量一個變量單獨存放的,它的基本存儲單元是 Cache Line,一般一個 Cache Line 的大小是 64 字節(jié),也就是說,一個 Cache Line 中可以存下 8 個 8 字節(jié)的 long 類型變量

那由于高速緩存的基本單元是 64 字節(jié)的 Cache Line,所以呢,緩存從內(nèi)存中一次讀取的數(shù)據(jù)就是 64 字節(jié)。換句話說,如果 cpu 要讀取一個 long 類型的數(shù)組,讀取其中一個元素的同時也會把接下來的其他相鄰地址的七個元素也加載到 Cache Line 中來。

這就會導(dǎo)致一個問題,舉個例子,我們定義了兩個 long 類型的變量 a 和 b,他們沒有關(guān)系,但是他們在內(nèi)存中的地址是緊挨著的,如果一個 CPU 核心的線程 T1 在對 a 進(jìn)行修改,另一個 CPU 核心的線程 T2 卻在對 b 進(jìn)行讀取。

那么當(dāng) T1 修改 a 的時候,除了把 a 加載到 Cache Line,還會把 b 同時也加載到 T1 所處 CPU 核心的 Cache Line 中來,對吧。

根據(jù) MESI 緩存一致性協(xié)議,修改完 a 后這個 Cache Line 的狀態(tài)就是 M(Modify,已修改),而其它所有包含 a 的 Cache Line 中的 a 就都不是最新值了,所以都將變?yōu)?I 狀態(tài)(Invalid,無效狀態(tài))

這樣,當(dāng) T2 來讀取 b 時,他就會發(fā)現(xiàn),他所處的 CPU 核心對應(yīng)的這個 Cache Line 已經(jīng)失效了,這樣他就需要花費比較長的時間從內(nèi)存中重新加載了。

也就是說,b 和 a 沒有任何關(guān)系,每次卻要因為 a 的更新導(dǎo)致他需要從內(nèi)存中重新讀取,拖慢了速度。這就是偽共享

對于偽共享,一般有兩種方法,其實思想都是一樣的:

1)padding:就是增大數(shù)組元素之間的間隔,使得不同線程存取的元素位于不同的緩存行上,以空間換時間。比如在 a 和 b 之間再定義 7 個 long 類型的變量,使得 a 和 b 不在一個 Cache Line 上,這樣當(dāng)修改 a 的時候 b 所處的 Cache Line 就不會受到影響了

2)JDK 1.8 提供了 @Contended 注解,就是把我們手動 padding 的操作封裝到這個注解里面了,把這個注解加在字段 a 或者 b 的上面就可以了


責(zé)任編輯:武曉燕 來源: 飛天小牛肉
相關(guān)推薦

2022-12-12 08:39:09

CPUCache偽共享

2018-07-27 10:56:10

2018-05-04 15:15:37

數(shù)據(jù)庫MySQL并發(fā)場景

2019-07-05 17:40:24

MySQL并發(fā)數(shù)據(jù)庫

2021-12-01 10:13:48

場景分布式并發(fā)

2019-01-15 14:44:02

CPU Cache L共享存儲器

2022-08-04 18:23:28

屏幕共享卡頓流暢度

2022-05-27 09:25:49

數(shù)據(jù)并發(fā)

2021-11-18 08:55:49

共享CPU內(nèi)存

2022-05-11 11:25:49

模型方案

2023-05-28 13:13:54

高并發(fā)場景JUC

2025-02-26 03:00:00

2025-02-28 00:03:22

高并發(fā)TPS系統(tǒng)

2019-12-17 14:24:11

CPU緩存偽共享

2017-07-13 16:40:16

偽共享緩存行存儲

2024-01-05 08:23:55

HttpClientQPS高并發(fā)

2024-07-25 09:05:35

2022-01-17 14:24:09

共享字節(jié)面試

2021-01-13 05:27:02

服務(wù)器性能高并發(fā)

2023-07-18 09:24:04

MySQL線程
點贊
收藏

51CTO技術(shù)棧公眾號