1.初識volatile
下面這段代碼,演示了一個(gè)使用 volatile 以及沒使用 volatile這個(gè)關(guān)鍵字阱缓,對于變量更新的影響 ,使用volatile來修改變量stop會保證主線程修改stop對t1線程的可見,不使用volatile的線程t1對主線程對stop變量的修改不可見举农。
/**
* @Project: ThreadExample
* @description: volatile 例子荆针,保證線程的可見性
* @author: sunkang
* @create: 2020-06-27 13:05
* @ModificationHistory who when What
**/
public class VolatileDemo {
public static volatile boolean stop=false;
public static void main( String[] args ) throws InterruptedException {
Thread t1=new Thread(()->{
int i=0;
while(!stop){ //condition 不滿足
i++;
System.out.println(i);
}
});
t1.start();
Thread.sleep(10);
stop=true; //true
}
}
2.volatile 的作用
可見性
:可見性是指多線程情況下線程能夠自動發(fā)現(xiàn)volatile變量的最新值。如果對Java內(nèi)存模型比較了解的話會知道颁糟,每個(gè)線程都會被分配一個(gè)線程棧航背,如果對象是多線程間的共享資源時(shí),當(dāng)線程訪問某一個(gè)對象時(shí)候值的時(shí)候棱貌,首先通過對象的引用找到對應(yīng)在堆內(nèi)存的變量的值玖媚,然后把堆內(nèi)存變量的值load到線程棧中,建立一個(gè)變量副本婚脱,之后線程操作的都是副本變量今魔,當(dāng)修改完副本變量之后,會寫回值到主內(nèi)存起惕。但由于線程棧是線程間相互隔離的涡贱,即多線程間不可見,如果有其他線程修改了這個(gè)變量惹想,但還未寫回主內(nèi)存或者更新主內(nèi)存后问词,其他線程讀取的仍是自己線程棧的副本時(shí),就會出現(xiàn)問題嘀粱。而volidate則是用來保證可見性激挪。即一個(gè)線程對共享變量的修改,能夠及時(shí)被其他線程看到锋叨。
指令重排序
:在執(zhí)行程序時(shí)為了提高性能垄分,編譯器和處理器常常會對指令做重排序。雖然代碼順序是有先后順序娃磺,但真正執(zhí)行時(shí)卻不一定按照代碼順序執(zhí)行薄湿。這樣在多線程下就可能存在問題。注意:只是可能出現(xiàn)問題,另外指令重排序在實(shí)際下發(fā)生情況比較少豺瘤,由于Java吆倦、CPU和內(nèi)存之間都有一套嚴(yán)格的指令重排序規(guī)則,具體可參照J(rèn)SR和JVM相關(guān)資料坐求。重排序分三種類型:
編譯器優(yōu)化的重排序
:編譯器在不改變單線程程序語義的前提下蚕泽,重新安排語句的執(zhí)行順序。
指令級并行的重排序
:現(xiàn)代處理器采用了指令級并行技術(shù)來將多條指令重疊執(zhí)行桥嗤。如果不存在數(shù)據(jù)依賴性须妻,處理器可以改變語句對應(yīng)機(jī)器指令的執(zhí)行順序。
內(nèi)存系統(tǒng)的重排序
:由于處理器使用緩存和讀/寫緩沖區(qū)泛领,這使得加載和存儲操作看上去可能是在亂序執(zhí)行荒吏。
不保證原子性
:原子性是指這個(gè)操作是不可中斷,要么全部執(zhí)行成功要么全部執(zhí)行失敗师逸,就算在多個(gè)線程一起執(zhí)行的時(shí)候司倚,一個(gè)操作一旦開始,就不會被其他線程所干擾篓像。volatile并不能保證原子操作动知,例如i++操作時(shí),分為Load员辩、Increment盒粮、Store、Memory Barriers四個(gè)步驟奠滑,即裝載丹皱、新增、存儲和內(nèi)存屏障四個(gè)步驟宋税,第四步則是保證jvm讓最新的變量值在所有線程可見摊崭,但從Load、Increment杰赛、到Store是不安全的呢簸,中間如果其他的CPU線程修改值將會存在問題。
3.volatile 關(guān)鍵字是如何保證可見性的
我們可以使用【hsdis】這個(gè)工具乏屯,來查看前面演示的這段 代碼的匯編指令根时,具體的使用請查看使用說明文檔 在運(yùn)行的代碼中,設(shè)置jvm參數(shù)如下 【-server -Xcomp -XX:+UnlockDiagnosticVMOptions XX:+PrintAssembly XX:CompileCommand=compileonly,App.(替換成實(shí)際 運(yùn)行的代碼) 】
然后在輸出的結(jié)果中辰晕,查找下lock指令蛤迎,會發(fā)現(xiàn),在修改帶有volatile修飾的成員變量時(shí)含友,會多一個(gè)lock指令替裆。lock 是一種控制指令校辩,在多處理器環(huán)境下,lock 匯編指令可以 基于總線鎖或者緩存鎖的機(jī)制來達(dá)到可見性的一個(gè)效果辆童。
為了讓大家更好的理解可見性的本質(zhì)召川,我們需要從硬件層 面進(jìn)行梳理
4.從硬件層面了解可見性的本質(zhì)
一臺計(jì)算機(jī)中最核心的組件是CPU、內(nèi)存胸遇、以及I/O設(shè)備。 在整個(gè)計(jì)算機(jī)的發(fā)展歷程中汉形,除了CPU纸镊、內(nèi)存以及I/O設(shè) 備不斷迭代升級來提升計(jì)算機(jī)處理性能之外,還有一個(gè)非 常核心的矛盾點(diǎn)概疆,就是這三者在處理速度的差異逗威。CPU的 計(jì)算速度是非常快的岔冀,內(nèi)存次之凯旭、最后是IO設(shè)備比如磁盤。 而在絕大部分的程序中使套,一定會存在內(nèi)存訪問罐呼,有些可能 還會存在I/O設(shè)備的訪問
為了提升計(jì)算性能,CPU從單核升級到了多核甚至用到了 超線程技術(shù)最大化提高 CPU 的處理性能侦高,但是僅僅提升 CPU性能還不夠嫉柴,如果后面兩者的處理性能沒有跟上,意 味著整體的計(jì)算效率取決于最慢的設(shè)備奉呛。為了平衡三者的速度差異计螺,最大化的利用CPU提升性能,從硬件瞧壮、操作系 統(tǒng)登馒、編譯器等方面都做出了很多的優(yōu)化
- CPU增加了高速緩存
- 操作系統(tǒng)增加了進(jìn)程、線程咆槽。通過CPU的時(shí)間片切換最 大化的提升CPU的使用率
- 編譯器的指令優(yōu)化陈轿,更合理的去利用好CPU的高速緩存 然后每一種優(yōu)化,都會帶來相應(yīng)的問題罗晕,而這些問題也是 導(dǎo)致線程安全性問題的根源济欢。為了了解前面提到的可見性 問題的本質(zhì),我們有必要去了解這些優(yōu)化的過程
CPU 高速緩存
線程是CPU調(diào)度的最小單元小渊,線程設(shè)計(jì)的目的最終仍然是 更充分的利用計(jì)算機(jī)處理的效能法褥,但是絕大部分的運(yùn)算任 務(wù)不能只依靠處理器“計(jì)算”就能完成,處理器還需要與內(nèi) 存交互酬屉,比如讀取運(yùn)算數(shù)據(jù)半等、存儲運(yùn)算結(jié)果揍愁,這個(gè) I/O 操 作是很難消除的。而由于計(jì)算機(jī)的存儲設(shè)備與處理器的運(yùn) 算速度差距非常大杀饵,所以現(xiàn)代計(jì)算機(jī)系統(tǒng)都會增加一層讀 寫速度盡可能接近處理器運(yùn)算速度的高速緩存來作為內(nèi)存 和處理器之間的緩沖:將運(yùn)算需要使用的數(shù)據(jù)復(fù)制到緩存 中莽囤,讓運(yùn)算能快速進(jìn)行,當(dāng)運(yùn)算結(jié)束后再從緩存同步到內(nèi) 存之中切距。
通過高速緩存的存儲交互很好的解決了處理器與內(nèi)存的速 度矛盾朽缎,但是也為計(jì)算機(jī)系統(tǒng)帶來了更高的復(fù)雜度,因?yàn)?它引入了一個(gè)新的問題谜悟,緩存一致性话肖。
什么叫緩存一致性呢
首先,有了高速緩存的存在以后葡幸,每個(gè)CPU的處理過程是最筒, 先將計(jì)算需要用到的數(shù)據(jù)緩存在CPU高速緩存中,在CPU 進(jìn)行計(jì)算時(shí)蔚叨,直接從高速緩存中讀取數(shù)據(jù)并且在計(jì)算完成 之后寫入到緩存中床蜘。在整個(gè)運(yùn)算過程完成后,再把緩存中 的數(shù)據(jù)同步到主內(nèi)存蔑水。
由于在多CPU種邢锯,每個(gè)線程可能會運(yùn)行在不同的CPU內(nèi), 并且每個(gè)線程擁有自己的高速緩存搀别。同一份數(shù)據(jù)可能會被 緩存到多個(gè) CPU 中弹囚,如果在不同 CPU 中運(yùn)行的不同線程
看到同一份內(nèi)存的緩存值不一樣就會存在緩存不一致的問 題
為了解決緩存不一致的問題,在 CPU 層面做了很多事情领曼, 主要提供了兩種解決辦法
- 總線鎖
- 緩存鎖
總線鎖和緩存鎖
總線鎖
鸥鹉,簡單來說就是,在多cpu下庶骄,當(dāng)其中一個(gè)處理器 要對共享內(nèi)存進(jìn)行操作的時(shí)候毁渗,在總線上發(fā)出一個(gè)LOCK# 信號,這個(gè)信號使得其他處理器無法通過總線來訪問到共 享內(nèi)存中的數(shù)據(jù)单刁,總線鎖定把CPU和內(nèi)存之間的通信鎖住 了灸异,這使得鎖定期間,其他處理器不能操作其他內(nèi)存地址 的數(shù)據(jù)羔飞,所以總線鎖定的開銷比較大肺樟,這種機(jī)制顯然是不 合適的
如何優(yōu)化呢?最好的方法就是控制鎖的保護(hù)粒度逻淌,我們只 需要保證對于被多個(gè) CPU 緩存的同一份數(shù)據(jù)是一致的就 行么伯。所以引入了緩存鎖,它核心機(jī)制是基于緩存一致性協(xié) 議來實(shí)現(xiàn)的卡儒。
緩存一致性協(xié)議
為了達(dá)到數(shù)據(jù)訪問的一致田柔,需要各個(gè)處理器在訪問緩存時(shí)遵循一些協(xié)議俐巴,在讀寫時(shí)根據(jù)協(xié)議來操作,常見的協(xié)議有 MSI硬爆,MESI欣舵,MOSI等。最常見的就是MESI協(xié)議缀磕。接下來 給大家簡單講解一下MESI MESI表示緩存行的四種狀態(tài)缘圈,分別是
- M(Modify) 表示共享數(shù)據(jù)只緩存在當(dāng)前 CPU 緩存中, 并且是被修改狀態(tài)袜蚕,也就是緩存的數(shù)據(jù)和主內(nèi)存中的數(shù)據(jù)不一致
- E(Exclusive) 表示緩存的獨(dú)占狀態(tài)准验,數(shù)據(jù)只緩存在當(dāng)前 CPU緩存中,并且沒有被修改
- S(Shared) 表示數(shù)據(jù)可能被多個(gè)CPU緩存廷没,并且各個(gè)緩 存中的數(shù)據(jù)和主內(nèi)存數(shù)據(jù)一致
- I(Invalid) 表示緩存已經(jīng)失效
在 MESI 協(xié)議中,每個(gè)緩存的緩存控制器不僅知道自己的 讀寫操作垂寥,而且也監(jiān)聽(snoop)其它Cache的讀寫操作
E狀態(tài)示例如下:
只有Core 0訪問變量x颠黎,它的Cache line狀態(tài)為E(Exclusive)。
S狀態(tài)示例如下:
3個(gè)Core都訪問變量x滞项,它們對應(yīng)的Cache line為S(Shared)狀態(tài)狭归。
M狀態(tài)和I狀態(tài)示例如下:
Core 0修改了x的值之后,這個(gè)Cache line變成了M(Modified)狀態(tài)文判,其他Core對應(yīng)的Cache line變成了I(Invalid)狀態(tài)过椎。
對于 MESI 協(xié)議,從 CPU 讀寫角度來說會遵循以下原則:
CPU讀請求:緩存處于M戏仓、E疚宇、S狀態(tài)都可以被讀取,I狀態(tài)CPU只能從主存中讀取數(shù)據(jù)
CPU寫請求:緩存處于M赏殃、E狀態(tài)才可以被寫敷待。對于S狀 態(tài)的寫,需要將其他CPU中緩
存行置為無效才可寫
使用總線鎖和緩存鎖機(jī)制之后仁热,CPU對于內(nèi)存的操作大概 可以抽象成下面這樣的結(jié)構(gòu)榜揖。從而達(dá)到緩存一致性效果
總結(jié)可見性的本質(zhì)
由于CPU 高速緩存的出現(xiàn)使得 如果多個(gè)cpu同時(shí)緩存了 相同的共享數(shù)據(jù)時(shí),可能存在可見性問題抗蠢。也就是CPU0修 改了自己本地緩存的值對于 CPU1 不可見举哟。不可見導(dǎo)致的 后果是 CPU1 后續(xù)在對該數(shù)據(jù)進(jìn)行寫入操作時(shí),是使用的臟數(shù)據(jù)迅矛。使得數(shù)據(jù)最終的結(jié)果不可預(yù)測妨猩。
了解到這里,大家應(yīng)該會有一個(gè)疑問秽褒,剛剛不是說基于緩存一致性協(xié)議或者總線鎖能夠達(dá)到緩存一致性的要求嗎册赛? 為什么還需要加 volatile 關(guān)鍵字钠导?或者說為什么還會存在 可見性問題呢?
MESI 優(yōu)化帶來的可見性問題
MESI協(xié)議雖然可以實(shí)現(xiàn)緩存的一致性森瘪,但是也會存在一些 問題牡属。
就是各個(gè)CPU緩存行的狀態(tài)是通過消息傳遞來進(jìn)行的。如 果 CPU0 要對一個(gè)在緩存中共享的變量進(jìn)行寫入扼睬,首先需 要發(fā)送一個(gè)失效的消息給到其他緩存了該數(shù)據(jù)的CPU逮栅。并 且要等到他們的確認(rèn)回執(zhí)。CPU0 在這段時(shí)間內(nèi)都會處于阻塞狀態(tài)窗宇。為了避免阻塞帶來的資源浪費(fèi)措伐。在cpu中引入 了Store Bufferes。
CPU0只需要在寫入共享數(shù)據(jù)時(shí)军俊,直接把數(shù)據(jù)寫入到store bufferes中侥加,同時(shí)發(fā)送invalidate消息,然后繼續(xù)去處理其 他指令粪躬。
當(dāng)收到其他所有CPU發(fā)送了invalidate acknowledge消息 時(shí)担败,再將 store bufferes 中的數(shù)據(jù)數(shù)據(jù)存儲至 cache line 中。最后再從緩存行同步到主內(nèi)存镰官。
但是這種優(yōu)化存在兩個(gè)問題
- 數(shù)據(jù)什么時(shí)候提交是不確定的提前,因?yàn)樾枰却渌?cpu 給回復(fù)才會進(jìn)行數(shù)據(jù)同步。這里其實(shí)是一個(gè)異步操作
- 引入了storebufferes后泳唠,處理器會先嘗試從storebuffer 中讀取值狈网,如果 storebuffer 中有數(shù)據(jù),則直接從 storebuffer中讀取笨腥,否則就再從緩存行中讀取
我們來看一個(gè)例子
exeToCPU0和exeToCPU1分別在兩個(gè)獨(dú)立的CPU上執(zhí)行拓哺。 假如 CPU0 的緩存行中緩存了 isFinish 這個(gè)共享變量,并 且狀態(tài)為(E)脖母、而Value可能是(S)狀態(tài)拓售。
那么這個(gè)時(shí)候,CPU0在執(zhí)行的時(shí)候镶奉,會先把value=10的 指令寫入到storebuffer中础淤。并且通知給其他緩存了該value 變量的CPU。在等待其他CPU通知結(jié)果的時(shí)候哨苛,CPU0會 繼續(xù)執(zhí)行isFinish=true這個(gè)指令鸽凶。 而因?yàn)楫?dāng)前CPU0緩存了isFinish并且是Exclusive狀態(tài),所以可以直接修改 isFinish=true。
這個(gè)時(shí)候 CPU1 發(fā)起 read 操作去讀取isFinish的值可能為true建峭,但是value的值不等于10玻侥。 這種情況我們可以認(rèn)為是CPU的亂序執(zhí)行,也可以認(rèn)為是一種重排序亿蒸,而這種重排序會帶來可見性的問題
這下硬件工程師也抓狂了凑兰,我們也能理解掌桩,從硬件層面很 難去知道軟件層面上的這種前后依賴關(guān)系,所以沒有辦法 通過某種手段自動去解決姑食。
所以在 CPU 層面提供了 memory barrier(內(nèi)存屏障)的指 令波岛,從硬件層面來看這個(gè)memroy barrier就是CPU flush store bufferes中的指令。軟件層面可以決定在適當(dāng)?shù)牡胤?來插入內(nèi)存屏障音半。
CPU 層面的內(nèi)存屏障
什么是內(nèi)存屏障?從前面的內(nèi)容基本能有一個(gè)初步的猜想则拷, 內(nèi)存屏障就是將 store bufferes 中的指令寫入到內(nèi)存,從 而使得其他訪問同一共享內(nèi)存的線程的可見性曹鸠。
X86的memory barrier指令包括lfence(讀屏障) sfence(寫 屏障) mfence(全屏障)
Store Memory Barrier(寫屏障) 告訴處理器在寫屏障之前 的所有已經(jīng)存儲在存儲緩存(store bufferes)中的數(shù)據(jù)同步 到主內(nèi)存煌茬,簡單來說就是使得寫屏障之前的指令的結(jié)果對 屏障之后的讀或者寫是可見的
Load Memory Barrier(讀屏障) 處理器在讀屏障之后的讀 操作,都在讀屏障之后執(zhí)行。配合寫屏障彻桃,使得寫屏障之前 的內(nèi)存更新對于讀屏障之后的讀操作是可見的
Full Memory Barrier(全屏障) 確保屏障前的內(nèi)存讀寫操作 的結(jié)果提交到內(nèi)存之后坛善,再執(zhí)行屏障后的讀寫操作
有了內(nèi)存屏障以后,對于上面這個(gè)例子邻眷,我們可以這么來 改眠屎,從而避免出現(xiàn)可見性問題
總的來說,內(nèi)存屏障的作用可以通過防止CPU對內(nèi)存的亂序訪問來保證共享數(shù)據(jù)在多線程并行執(zhí)行下的可見性
但是這個(gè)屏障怎么來加呢耗溜?回到最開始我們講 volatile 關(guān) 鍵字的代碼,這個(gè)關(guān)鍵字會生成一個(gè)Lock的匯編指令省容,這 個(gè)指令其實(shí)就相當(dāng)于實(shí)現(xiàn)了一種內(nèi)存屏障
這個(gè)時(shí)候問題又來了抖拴,內(nèi)存屏障、重排序這些東西好像是 和平臺以及硬件架構(gòu)有關(guān)系的腥椒。作為Java語言的特性阿宅,一 次編寫多處運(yùn)行。我們不應(yīng)該考慮平臺相關(guān)的問題笼蛛,并且 這些所謂的內(nèi)存屏障也不應(yīng)該讓程序員來關(guān)心洒放。
5.JMM
什么是 JMM
JMM全稱是Java Memory Model. 什么是JMM呢? 通過前面的分析發(fā)現(xiàn)滨砍,導(dǎo)致可見性問題的根本原因是緩存 以及重排序往湿。 而JMM實(shí)際上就是提供了合理的禁用緩存 以及禁止重排序的方法。所以它最核心的價(jià)值在于解決可 見性和有序性惋戏。 JMM屬于語言級別的抽象內(nèi)存模型领追,可以簡單理解為對硬 件模型的抽象,它定義了共享內(nèi)存中多線程程序讀寫操作 的行為規(guī)范:在虛擬機(jī)中把共享變量存儲到內(nèi)存以及從內(nèi) 存中取出共享變量的底層實(shí)現(xiàn)細(xì)節(jié)
通過這些規(guī)則來規(guī)范對內(nèi)存的讀寫操作從而保證指令的正 確性响逢,它解決了CPU多級緩存绒窑、處理器優(yōu)化、指令重排序 導(dǎo)致的內(nèi)存訪問問題舔亭,保證了并發(fā)場景下的可見性些膨。
需要注意的是蟀俊,JMM并沒有限制執(zhí)行引擎使用處理器的寄 存器或者高速緩存來提升指令執(zhí)行速度,也沒有限制編譯 器對指令進(jìn)行重排序订雾,也就是說在JMM中肢预,也會存在緩存 一致性問題和指令重排序問題。只是JMM把底層的問題抽 象到 JVM 層面葬燎,再基于 CPU 層面提供的內(nèi)存屏障指令误甚, 以及限制編譯器的重排序來解決并發(fā)問題
JMM抽象模型分為主內(nèi)存、工作內(nèi)存谱净;主內(nèi)存是所有線程 共享的窑邦,一般是實(shí)例對象、靜態(tài)字段壕探、數(shù)組對象等存儲在 堆內(nèi)存中的變量冈钦。工作內(nèi)存是每個(gè)線程獨(dú)占的,線程對變 量的所有操作都必須在工作內(nèi)存中進(jìn)行李请,不能直接讀寫主 內(nèi)存中的變量瞧筛,線程之間的共享變量值的傳遞都是基于主 內(nèi)存來完成
Java 內(nèi)存模型底層實(shí)現(xiàn)可以簡單的認(rèn)為:通過內(nèi)存屏障 (memory barrier)禁止重排序,即時(shí)編譯器根據(jù)具體的底層 體系架構(gòu)导盅,將這些內(nèi)存屏障替換成具體的 CPU 指令较幌。對 于編譯器而言接箫,內(nèi)存屏障將限制它所能做的重排序優(yōu)化洪规。 而對于處理器而言殴穴,內(nèi)存屏障將會導(dǎo)致緩存的刷新操作刁笙。 比如日缨,對于 volatile乔外,編譯器將在 volatile 字段的讀寫操作前后各插入一些內(nèi)存屏障视卢。
JMM 是如何解決可見性有序性問題的
簡單來說仿滔,JMM提供了一些禁用緩存以及進(jìn)制重排序的方 法巢株,來解決可見性和有序性問題槐瑞。這些方法大家都很熟悉: volatile、synchronized阁苞、final困檩;
JMM 如何解決順序一致性問題
重排序問題
為了提高程序的執(zhí)行性能,編譯器和處理器都會對指令做重排序那槽,其中處理器的重排序在前面已經(jīng)分析過了窗看。所謂的重排序其實(shí)就是指執(zhí)行的指令順序
編譯器的重排序指的是程序編寫的指令在編譯之后,指令可能會產(chǎn)生重排序來優(yōu)化程序的執(zhí)行性能倦炒。 從源代碼到最終執(zhí)行的指令显沈,可能會經(jīng)過三種重排序。
2 和 3 屬于處理器重排序。這些重排序可能會導(dǎo)致可見性問題拉讯。
編譯器的重排序涤浇,JMM提供了禁止特定類型的編譯器重排序。
處理器重排序魔慷,JMM會要求編譯器生成指令時(shí)只锭,會插入內(nèi)存屏障來禁止處理器重排序
當(dāng)然并不是所有的程序都會出現(xiàn)重排序問題 編譯器的重排序和CPU的重排序的原則一樣,會遵守?cái)?shù)據(jù) 依賴性原則院尔,編譯器和處理器不會改變存在數(shù)據(jù)依賴關(guān)系 的兩個(gè)操作的執(zhí)行順序,比如下面的代碼蜻展,
a=1; b=a;
a=1;a=2;
a=b;b=1;
這三種情況在單線程里面如果改變代碼的執(zhí)行順序,都會 導(dǎo)致結(jié)果不一致邀摆,所以重排序不會對這類的指令做優(yōu)化纵顾。 這種規(guī)則也成為 as-if-serial。
不管怎么重排序栋盹,對于單個(gè)線程來說執(zhí)行結(jié)果不能改變施逾。比如
int a=2; //1
int b=3; //2
int rs=a*b; //3
1 和 3、2 和 3 存在數(shù)據(jù)依賴例获,所以在最終執(zhí)行的指令中汉额, 3不能重排序到1和2之前,否則程序會報(bào)錯(cuò)榨汤。由于1和2 不存在數(shù)據(jù)依賴蠕搜,所以可以重新排列1和2的順序
JMM 層面的內(nèi)存屏障
為了保證內(nèi)存可見性,Java 編譯器在生成指令序列的適當(dāng)位置會插入內(nèi)存屏障來禁止特定類型的處理器的重排序收壕, 在JMM中把內(nèi)存屏障分為四類
6.HappenBefore
它的意思表示的是前一個(gè)操作的結(jié)果對于后續(xù)操作是可見 的妓灌,所以它是一種表達(dá)多個(gè)線程之間對于內(nèi)存的可見性。 所以我們可以認(rèn)為在JMM中啼器,如果一個(gè)操作執(zhí)行的結(jié)果需 要對另一個(gè)操作可見旬渠,那么這兩個(gè)操作必須要存在 happens-before關(guān)系俱萍。這兩個(gè)操作可以是同一個(gè)線程端壳,也 可以是不同的線程
JMM 中有哪些方法建立 happen-before 規(guī)則
程序順序規(guī)則
- 一個(gè)
線程中
的每個(gè)操作,happens-before于該線程中的任意后續(xù)操作; 可以簡單認(rèn)為是as-if-serial枪蘑。 單個(gè)線程中的代碼順序不管怎么變损谦,對于結(jié)果來說是不變的
順序規(guī)則表示 1 happenns-before 2; 3 happensbefore 4
public class VolatileDemo {
int a=0;
volatile boolean flag=false;
public void writer(){ //線程A
a=1; //1
flag=true; //2
}
public void reader(){
if(flag){ //3
int x=a; //4
}
}
}
-
volatile變量規(guī)則
,對于volatile修飾的變量的寫的操作岳颇, 一定happen-before后續(xù)對于volatile變量的讀操作照捡;
根據(jù)volatile規(guī)則,2 happens before 3 -
傳遞性規(guī)則
话侧,如果 1 happens-before 2; 3happensbefore 4; 那么傳遞性規(guī)則表示: 1 happens-before 4;
public class VolatileDemo {
int a=0;
volatile boolean flag=false;
public void writer(){ //線程A
a=1; //1
flag=true; //2
}
public void reader(){
if(flag){ //3
int x=a; //4
}
}
}
-
start規(guī)則
栗精,如果線程A執(zhí)行操作ThreadB.start(),那么線 程 A 的 ThreadB.start()操作 happens-before 線程 B 中
的任意操作
public class StartRule {
static int x=0;
public static void main(String[] args) {
Thread t1=new Thread(()->{
//use x=10 //t1線程可以看到變量x被修改為10
});
x=10;
t1.start();
}
}
-
join規(guī)則
,如果線程A執(zhí)行操作ThreadB.join()并成功返 回,那么線程 B 中的任意操作 happens-before 于線程 A從ThreadB.join()操作成功返回
public class JoinRule {
static int x=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
x=100;
});
t1.start();
t1.join();
System.out.println(x); //輸出100
}
}
-
監(jiān)視器鎖的規(guī)則
悲立,對一個(gè)鎖的解鎖鹿寨,happens-before于 隨后對這個(gè)鎖的加鎖
public class SyncDemo {
int x=10;
public void setValue() {
synchronized (this) {//ThreadA / ThreadB
if(this.x <12){
this.x=12;
System.out.println(x);//只會輸出一次12
}
}//此處自動解鎖
}
public static void main(String[] args) {
SyncDemo sync = new SyncDemo();
new Thread(()->sync.setValue()).start();
new Thread(()->sync.setValue()).start();
}
}
假設(shè) x 的初始值是 10,線程 A 執(zhí)行完代碼塊后 x 的 值會變成 12(執(zhí)行完自動釋放鎖)薪夕,線程 B 進(jìn)入代碼塊 時(shí)脚草,能夠看到線程 A 對 x 的寫操作,也就是線程 B 能 夠看到 x==12原献。