所屬文集:一起掌握并發(fā)
了解重排序
為了性能蹦误,編譯時和運(yùn)行時都會有重排序咱筛,造成指令執(zhí)行順序變了,宏觀上從這3點(diǎn)了解重排序:
- 線程內(nèi)有序:如果再本線程內(nèi)觀察嫌佑,所有的操作都是有序的豆茫,即線程內(nèi)表現(xiàn)為串行的語義(Within Thread As-If-Serial Semantics)。
- 線程間無序:如果再一個線程中觀察另一個線程歧强,所有的操作都是無序的澜薄,即 指指令重排序現(xiàn)象和工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象为肮。
- 總會有重排序:指令重排序在任何時候都有可能發(fā)生摊册,與是否為多線程無關(guān),之所以在單線程下感覺沒有發(fā)生重排序颊艳,是因?yàn)榫€程內(nèi)表現(xiàn)為串行的語義的存在茅特。
引起可見性問題的病因
從修復(fù)Java內(nèi)存模型忘分,第2部分里的一段話開始思考
理解JMM所需的關(guān)鍵概念之一是可見性 -您如何知道如果線程A執(zhí)行someVariable = 3,其他線程將看到線程A在其中 寫入的值3白修?存在許多原因妒峦,為什么另一個線程可能不會立即為以下值看到值3 someVariable:可能是因?yàn)榫幾g器已對指令進(jìn)行了重新排序以便更有效地執(zhí)行,或者已將someVariable其緩存在寄存器中兵睛,或者將其值寫入了緩存在寫處理器上但尚未刷新到主內(nèi)存肯骇,或者在讀處理器的緩存中有舊的(或陳舊的)值。內(nèi)存模型確定線程何時可以可靠地“看到”對其他線程所做的變量的寫入祖很。特別是笛丙,內(nèi)存模型為定義了語義volatile,synchronized假颇,final這確保了跨線程的內(nèi)存操作可見性胚鸯。
總結(jié)為:導(dǎo)致可見性的原因有很多
- 為了提升性能而實(shí)施的編譯期重排序。
- 數(shù)據(jù)在寄存器中笨鸡。
- cpu緩存的更改未同步到主內(nèi)存中 或 內(nèi)存中的更改未同步到cpu緩存(運(yùn)行期重排序)姜钳。
- 。形耗。哥桥。
這里兩次提到重排序,那就先看重排序激涤。
重排序 遵守 as-if-serial語義
as-if-serial語義 : 不管怎么重排序(編譯器和處理器為了提高并行度)泰讽,程序在單線程中的執(zhí)行結(jié)果不會改變。
編譯器昔期、runtime和處理器都必須遵守as-if-serial語義:
為了遵守as-if-serial語義已卸,編譯器和處理器不會對存在數(shù)據(jù)依賴關(guān)系的操作做重排序,因?yàn)檫@種重排序會改變執(zhí)行結(jié)果硼一。
如果操作之間不存在數(shù)據(jù)依賴關(guān)系累澡,這些操作就可能被編譯器和處理器重排序
正確的理解as-if-serial語義的功效:
- 如果再本線程內(nèi)觀察,所有的操作都是有序的般贼;
- 如果再一個線程中觀察另一個線程愧哟,所有的操作都是無序的。
重排序的類型:
在執(zhí)行程序時為了提高性能哼蛆,編譯器和處理器常常會對指令做重排序蕊梧。重排序分三種類型:
編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義的前提下腮介,可以重新安排語句的執(zhí)行順序肥矢。屬于編譯期重排序。
指令級并行的重排序〉矗現(xiàn)代處理器采用了指令級并行技術(shù)(Instruction-Level Parallelism甘改, ILP)來將多條指令重疊執(zhí)行旅东。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對應(yīng)機(jī)器指令的執(zhí)行順序十艾。屬于運(yùn)行期重排序抵代。
內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀 / 寫緩沖區(qū)忘嫉,這使得加載和存儲操作看上去可能是在亂序執(zhí)行荤牍,屬于運(yùn)行期重排序。
從編譯運(yùn)行視角分為兩類:
- 編譯期重排序:包括 編譯器優(yōu)化的重排序庆冕。
- 運(yùn)行期重排序:包括 指令級并行的重排序参淫,內(nèi)存系統(tǒng)的重排序。
理解什么叫運(yùn)行期重排序
解讀上圖:已變更的數(shù)據(jù)立即寫到內(nèi)存太慢愧杯,所以先寫到Store Buffer.
舉個例子涎才,廚師飯做好了,不會直接端給你力九,而是放到那里等服務(wù)員端給你耍铜,無論服務(wù)員什么時候端給你,都不算是做好了直接給你吃跌前,而是放置了一會兒棕兼。
解讀上圖:其他cpu通知說,我緩存的數(shù)據(jù)無效了抵乓,但是我在忙別的伴挚,不想打斷正在做的事情,于是提供了一個通知隊(duì)列灾炭,讓他們把緩存無效的通知先放到 通知隊(duì)列中茎芋,等我忙完了再去處理通知
store buffer
- 優(yōu)點(diǎn):
- 可以保證core內(nèi)的指令流水線持續(xù)運(yùn)行,
- 它可以避免由于處理器停頓下來等待向內(nèi)存寫入數(shù)據(jù)而產(chǎn)生的延遲蜈出。
- 通過以批處理的方式刷新寫緩沖區(qū)田弥,以及合并寫緩沖區(qū)中對同一內(nèi)存地址的多次寫,可以減少對內(nèi)存總線的占用铡原。
- 缺點(diǎn):
每個處理器上的寫緩沖區(qū)偷厦,僅對它所在的處理器可見。這個特性會對內(nèi)存操作的執(zhí)行順序產(chǎn)生重要的影響:處理器對內(nèi)存的寫操作的執(zhí)行順序燕刻,不一定與內(nèi)存實(shí)際發(fā)生的寫操作順序一致只泼!
Invalidate Queue
- 優(yōu)點(diǎn):
正在處理的事情不中斷 - 缺點(diǎn):
處理器對內(nèi)存的讀操作的執(zhí)行順序,不一定與內(nèi)存實(shí)際發(fā)生的寫操作順序一致卵洗!使用已經(jīng)過期的數(shù)據(jù)请唱,而不是最新的。
在兩個CPU同時運(yùn)行的情況下,CPU0自身視角來說籍滴,沒有重排發(fā)生酪夷,一切都那么自然榴啸,但是CPU1卻看到CPU0發(fā)生了重排(reordering memory)孽惰。這就是內(nèi)存系統(tǒng)重排序。
禁止運(yùn)行期重排序
store buffer 和 Invalidate Queue 帶來的亂序如何解決
CPU通常提供了內(nèi)存屏障指令鸥印,來解決這樣的亂序問題勋功。讀屏障,清空本地的invalidate queue库说,保證之前的所有l(wèi)oad都已經(jīng)生效狂鞋;寫屏障,清空本地的store buffer潜的,使得之前的所有store操作都生效骚揍。
通俗來說就是兩點(diǎn):
寫屏障:保證把更新寫到內(nèi)存
讀屏障:保證從內(nèi)存讀取最新數(shù)據(jù)
JMM把內(nèi)存屏障分為四類,其實(shí)就是讀啰挪、寫屏障的組合:
JMM 的處理器重排序規(guī)則 會 要求 java 編譯器在生成指令序列時信不,插入特定類型的內(nèi)存屏障指令,來禁止特定類型的處理器重排序(不是所有的處理器重排序都要禁止)亡呵。
理解什么叫編譯期重排序:
private int val = 0;
private volatile boolean stop = false;
...
fun(){
val = 10;//代碼1
stop = true;//代碼2
}
這里代碼1 和代碼2 編譯后順序不會調(diào)整很容易理解抽活,但是下邊這個例子就有歧義了:
private boolean stop ;
while(!stop){
i++;
}
按照java中volatile關(guān)鍵字的疑惑中大濕的解答
這種代碼會被編譯優(yōu)化成類似
if(!stop){
while(true){
i++;
}
}
這種答案我還是不太信服的,看官您若有答案請留言告知哦锰什。
大師的答案中用到了HSDIS技術(shù)下硕,深入解析volatile關(guān)鍵字 對HSDIS的講解很全面
禁止編譯期重排序
JMM 的編譯器重排序規(guī)則 會 禁止 特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。
java中禁止重排序的操作
synchronized鎖
題外話之總線風(fēng)暴
總線風(fēng)暴:總線帶寬達(dá)到峰值汁胆;原因:
- 內(nèi)存屏障從主內(nèi)存嗅探
- cas不斷循環(huán)無效交互導(dǎo)致
解決辦法:
部分volatile和cas使用synchronize
Happens-Before 規(guī)則 關(guān)注使用效果而非實(shí)現(xiàn)梭姓。
上述內(nèi)容應(yīng)該也只是保證可見性的一部分內(nèi)容,我本身還困惑寄存器緩存和指令并行重排等情況嫩码,作為上層高級語言程序員糊昙,很難很難掌握全部底層的情況。
通過一種更簡單的方式谢谦,結(jié)合 java語法释牺,以使用的視角來掌握如何保證可見性,而不是 如何實(shí)現(xiàn)可見性保證回挽。很自然的happens-before的概念就是這個作用没咙,告訴程序員怎么用。 而保證可見性的實(shí)現(xiàn)則是JMM自己的事情千劈,不是程序員的祭刚。
從 JDK5 開始 java 使用新的 JSR -133 內(nèi)存模型,并依據(jù)此內(nèi)存模型提出了 happens-before 的概念,通過這個概念來闡述操作之間的內(nèi)存可見性涡驮。
我們要保證可見性暗甥,就是遵守Happens-Before 規(guī)則,合理的使用java提供的工具捉捅。