1 cpu術(shù)語的定義
術(shù)語 | 英文單詞 | 術(shù)語描述 |
---|---|---|
內(nèi)存屏障 | memory barriers | 是一組處理器指令,用于實(shí)現(xiàn)內(nèi)存操作的順序限制 |
緩沖行 | cache line | 緩存中可以分配的最小存儲(chǔ)單位。處理器填寫緩存線時(shí)會(huì)加載整個(gè)緩存線,需要使用多個(gè)主內(nèi)存讀周期 |
原子操作 | atomic operations | 不可中斷的一個(gè)或一系列操作 |
緩存行填充 | cache line fill | 當(dāng)處理器識(shí)別到從內(nèi)存中讀取操作數(shù)是可緩存的,處理器讀取整個(gè)緩存行到適當(dāng)?shù)木彺妫↙1,L2,L3或所有) |
緩存命中 | cache hit | 如果進(jìn)行高速緩存行填充操作的內(nèi)存位置仍然是下次處理器訪問的地址時(shí),處理器從緩存中讀取操作數(shù),而不是從內(nèi)存讀取 |
寫命中 | write hit | 當(dāng)處理器將操作數(shù)寫回到一個(gè)內(nèi)存緩存的區(qū)域時(shí),它首先會(huì)檢查這個(gè)緩存的內(nèi)存地址是否在緩存行中,如果存在一個(gè)有效的緩存行,則處理器將這個(gè)操作數(shù)寫回到緩存,而不是寫回到內(nèi)存,這個(gè)操作被稱為寫命中 |
寫缺失 | write misses the cache | 一個(gè)有效的緩存行被寫入到不存在的內(nèi)存區(qū)域 |
2. volatile的定義
Java語言規(guī)范第3版中對(duì)volatile的定義如下:
Java編程語言允許線程訪問共享變量,為了確保共享變量能被準(zhǔn)確和一致地更新,線程應(yīng)該確保通過排他鎖單獨(dú)獲得這個(gè)變量。Java語言提供了volatile,在某些情況下比鎖要更加方便。如果一個(gè)字段被聲明成volatile,Java線程內(nèi)存模型確保所有線程看到這個(gè)變量的值是一致的。
即使是64位的long型和double型變量,只要它是volatile變量,對(duì)該變量的讀/寫就具有原子性。如果是多個(gè)volatile操作或類似于volatile++這種復(fù)合操作,這些操作整體上不具原子性
- volatile變量自身具有一下特性:
- 可見性。對(duì)一個(gè)volatile變量的讀,總是能看到(任意線程)對(duì)這個(gè)volatile變量最后的寫入。
- 原子性:對(duì)任意單個(gè)volatile變量的讀/寫具有原子性,但類似于volatile++這種復(fù)合操作不具有原子性。
3 volatile寫-讀的內(nèi)存語義
- volatile寫的內(nèi)存語義:當(dāng)寫一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存。
- volatile讀的內(nèi)存語義:當(dāng)讀一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無效。線程接下來將從主內(nèi)存中讀取共享變量。
volatile重排序規(guī)。
volatile重排序規(guī)則表.jpg
從上表可以看出
- 當(dāng)?shù)诙€(gè)操作是volatile寫時(shí),不管第一個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保volatile寫之前的操作不會(huì)被編譯器重排序到volatile寫之后。
- 當(dāng)?shù)谝粋€(gè)操作是volatile讀時(shí),不管第二個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保volatile讀之后的操作不會(huì)被編譯器重排序到volatile讀之前。
- 當(dāng)?shù)谝粋€(gè)操作是volatile寫,第二個(gè)操作是volatile讀時(shí),不能重排序。
4 volatile 內(nèi)存語義的實(shí)現(xiàn)
- 為了實(shí)現(xiàn)volatile的內(nèi)存語義,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。
- 對(duì)于編譯器來說,發(fā)現(xiàn)一個(gè)最優(yōu)布置來最小化插入屏障的總數(shù)幾乎不可能。
4.1 JMM采取保守策略
- JMM采取保守策略。下面是基于保守策略的JMM內(nèi)存屏障插入策略
- 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障。
- 在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障。
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障
- 上述內(nèi)存屏障插入策略非常保守,但它可以保證在任意處理器平臺(tái),任意的程序中都能得到正確的volatile內(nèi)存語義
4.2 volatile寫插入內(nèi)存屏障后生成的指令序列
qq_pic_merged_1533870644878.jpg
- StoreStore屏障可以保證在volatile寫之前,其前面的所有普通寫操作已經(jīng)對(duì)任意處理器可見了。這是因?yàn)镾toreStore屏障將保障上面所有的普通寫在volatile寫之前刷新到主內(nèi)存。
- volatile寫后面的StoreLoad屏障。此屏障的作用是避免volatile寫與后面可能有的volatile讀/寫操作重排序。因?yàn)榫幾g器常常無法準(zhǔn)確判斷在一個(gè)volatile寫的后面是否需要插入一個(gè)StoreLoad屏障(比如,一個(gè)volatile寫之后方法立即return)。
- 為了保證能正確實(shí)現(xiàn)volatile的內(nèi)存語義,JMM在采取了保守策略:在每個(gè)volatile寫的后面,或者在每個(gè)volatile讀的前面插入一個(gè)StoreLoad屏障。
- 從整體執(zhí)行效率的角度考慮,JMM最終選擇了在每個(gè)volatile寫的后面插入一個(gè)StoreLoad屏障。因?yàn)関olatile寫-讀內(nèi)存語義的常見使用模式是:一個(gè)寫線程寫volatile變量,多個(gè)讀線程讀同一個(gè)volatile變量。
- 當(dāng)讀線程的數(shù)量大大超過寫線程時(shí),選擇在volatile寫之后插入StoreLoad屏障將帶來可觀的執(zhí)行效率的提升。從這里可以看到JMM在實(shí)現(xiàn)上的一個(gè)特點(diǎn):首先確保正確性,然后再去追求執(zhí)行效率。
4.3 volatile讀插入內(nèi)存屏障后生成的指令序列
volatile讀插入內(nèi)存屏障后生成的指令序列.jpg
- LoadLoad屏障用來禁止處理器把上面的volatile讀與下面的普通讀重排序。
- LoadStore屏障用來禁止處理器把上面的volatile讀與下面的普通寫重排序。
- 在實(shí)際執(zhí)行時(shí),只要不改變volatile寫-讀的內(nèi)存語義,編譯器可以根據(jù)具體情況省略不必要的屏障。
4.4示例
class VolatileBarrierExample {
int a;
volatile int v1 = 1;
volatile int v2 = 2;
void readAndWrite() {
int i = v1; // 第一個(gè)volatile讀
int j = v2; // 第二個(gè)volatile讀
a = i + j; // 普通寫
v1 = i + 1; // 第一個(gè)volatile寫
v2 = j * 2; // 第二個(gè) volatile寫
}
… // 其他方法
}
針對(duì)readAndWrite()方法,編譯器在生成字節(jié)碼時(shí)可以做如下的優(yōu)化。
qq_pic_merged_1533871374155.jpg
- 注意,最后的StoreLoad屏障不能省略。因?yàn)榈诙€(gè)volatile寫之后,方法立即return。此時(shí)編譯器可能無法準(zhǔn)確斷定后面是否會(huì)有volatile讀或?qū)懀瑸榱税踩鹨?,編譯器通常會(huì)在這里插入一個(gè)StoreLoad屏障。
- 上面的優(yōu)化針對(duì)任意處理器平臺(tái)
4.5 不同的處理器(X86)
- 由于不同的處理器有不同“松緊度”的處理器內(nèi)存模型,內(nèi)存屏障的插入還可以根據(jù)具體的處理器內(nèi)存模型繼續(xù)優(yōu)化。
- 以X86處理器為例,除最后的StoreLoad屏障外,其他的屏障都會(huì)被省略。
- X86處理器僅會(huì)對(duì)寫-讀操作做重排序。X86不會(huì)對(duì)讀-讀、讀-寫和寫-寫操作做重排序,因此在X86處理器中會(huì)省略掉這3種操作類型對(duì)應(yīng)的內(nèi)存屏障。
- 在X86中,JMM僅需在volatile寫后面插入一個(gè)StoreLoad屏障即可正確實(shí)現(xiàn)volatile寫-讀的內(nèi)存語義。這意味著在X86處理器中,volatile寫的開銷比volatile讀的開銷會(huì)大很多(因?yàn)閳?zhí)行StoreLoad屏障開銷會(huì)比較大)
qq_pic_merged_1533871668360.jpg
5 其他
- 在JSR-133之前的舊Java內(nèi)存模型中,雖然不允許volatile變量之間重排序,但舊的Java內(nèi)存模型允許volatile變量與普通變量重排序。
- JSR-133專家組決定增強(qiáng)volatile的內(nèi)存語義:嚴(yán)格限制編譯器和處理器對(duì)volatile變量與普通變量的重排序,確保volatile的寫-讀和鎖的釋放-獲取具有相同的內(nèi)存語義。從編譯器重排序規(guī)則和處理器內(nèi)存屏障插入策略來看,只要volatile變量與普通變量之間的重排序可能會(huì)破壞volatile的內(nèi)存語義,這種重排序就會(huì)被編譯器重排序規(guī)則和處理器內(nèi)存屏障插入策略禁止。
- 由于volatile僅僅保證對(duì)單個(gè)volatile變量的讀/寫具有原子性,而鎖的互斥執(zhí)行的特性可以確保對(duì)整個(gè)臨界區(qū)代碼的執(zhí)行具有原子性。在功能上,鎖比volatile更強(qiáng)大;在可伸縮性和執(zhí)行性能上,volatile更有優(yōu)勢(shì)。
參考
《java并發(fā)編程的藝術(shù)》