Java多線程之內(nèi)存可見(jiàn)性(筆記)

可見(jiàn)性:

一個(gè)線程對(duì)共享變量值的修改,能夠及時(shí)地被其他線程看到。

共享變量:

如果一個(gè)變量在多個(gè)線程的工作內(nèi)存中都存在副本,那么這個(gè)變量就是這幾個(gè)線程的共享變量。

Java內(nèi)存模型(JMM)

Java內(nèi)存模型(Java Memory Model)描述了Java程序中各種變量(線程共享變量)的訪問(wèn)規(guī)則净捅,以及在JVM中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中讀取出變量這樣的底層細(xì)節(jié)。

這里的變量指的是:共享變量

1辩块、所有的變量都存儲(chǔ)在主內(nèi)存中

2蛔六、每個(gè)線程都有自己獨(dú)立的工作內(nèi)存,里面保存該線程使用到的變量的副本(主內(nèi)存中該變量的一份拷貝)

X 變量就是共享變量:


image.png

可見(jiàn)性的兩條規(guī)定:

1废亭、線程對(duì)共享變量的所有操作都必須在自己的工作內(nèi)存中進(jìn)行国章,不能直接從主內(nèi)存中讀寫(xiě)

2、不同線程之間無(wú)法直接訪問(wèn)其他線程工作內(nèi)存中的變量豆村,線程間變量值的傳遞需要通過(guò)主內(nèi)存來(lái)完成液兽。

共享變量可見(jiàn)性實(shí)現(xiàn)的原理

線程1對(duì)共享變量的修改要想被線程2及時(shí)看到,必須要經(jīng)過(guò)如下2個(gè)步驟:

1掌动、把工作內(nèi)存1中更新過(guò)的共享變量刷新到主內(nèi)存中

2四啰、將主內(nèi)存中最新的共享變量的值更新到工作內(nèi)存2中

示例:

1、首先主內(nèi)存和工作內(nèi)存中 X 的值都為0


?
2粗恢、線程1將自己工作內(nèi)存中 X 的值修改為1柑晒,如圖:


?
3、為了將線程1修改的值更新到線程2的工作內(nèi)存中适滓,需要經(jīng)過(guò)兩個(gè)步驟:首先是將線程1的工作內(nèi)存中的值更新到主內(nèi)存當(dāng)中敦迄。


?
4恋追、最后將主內(nèi)存中 X 的值更新到線程2的工作內(nèi)存當(dāng)中凭迹。這就可以說(shuō)線程1對(duì) X 的修改被線程2及時(shí)的看到了。


?

要實(shí)現(xiàn)共享變量的可見(jiàn)性苦囱,必須保證兩點(diǎn):

1嗅绸、線程修改后的共享變量值能夠及時(shí)從工作內(nèi)存刷新到主內(nèi)存中
2、其他線程能夠及時(shí)把共享變量的最新值從主內(nèi)存更新到自己的工作內(nèi)存中

?

可見(jiàn)性的實(shí)現(xiàn)方式

Java語(yǔ)言層面支持的可見(jiàn)性實(shí)現(xiàn)方式:

  • synchronized

  • volatile

?

synchronized實(shí)現(xiàn)可見(jiàn)性

synchronized能夠?qū)崿F(xiàn):

  • 原子性(同步)
  • 可見(jiàn)性

?

JMM關(guān)于synchronized的兩條規(guī)定:

1撕彤、線程解鎖前鱼鸠,必須把共享變量的最新值刷新到主內(nèi)存中

2、線程加鎖時(shí)羹铅,將清空工作內(nèi)存中共享變量的值蚀狰,從而使用共享變量時(shí)需要從主內(nèi)存中重新讀取最新的值(注意:加鎖與解鎖需要是同一把鎖)

從而就實(shí)現(xiàn)了線程解鎖前對(duì)共享變量的修改在下次加鎖時(shí)對(duì)其他線程可見(jiàn)

?

線程執(zhí)行互斥代碼的過(guò)程:

1.獲得互斥鎖

2.清空工作內(nèi)存

3.從主內(nèi)存拷貝變量的最新副本到工作內(nèi)存

4.執(zhí)行代碼

5.將更改后的共享變量的值刷新到主內(nèi)存

6.釋放互斥鎖

?

重排序

重排序:代碼書(shū)寫(xiě)的順序與實(shí)際執(zhí)行的順序不同,指令重排序是編譯器或處理器為了提高程序性能而做的優(yōu)化

1.編譯器優(yōu)化的重排序(編譯器優(yōu)化)
在單線程中职员,保證執(zhí)行結(jié)果正確的情況下麻蹋,改變代碼的執(zhí)行順序。

2.指令級(jí)并行重排序(處理器優(yōu)化)

3.內(nèi)存系統(tǒng)的重排序(處理器優(yōu)化)
指的是對(duì)讀寫(xiě)緩存進(jìn)行的優(yōu)化

示例:重排序就是有可能出現(xiàn)下面的情況:


?

as-if-serial 語(yǔ)義

as-if-serial:無(wú)論如何重排序焊切,程序執(zhí)行的結(jié)果應(yīng)該與代碼順序執(zhí)行的結(jié)果一致(Java編譯器扮授、運(yùn)行時(shí)和處理器都會(huì)保證Java在單線程下遵循as-if-serial語(yǔ)義)


?
單線程:第1芳室、2行的順序可以重排,但第3行不能

重排序不會(huì)給單線程帶來(lái)內(nèi)存可見(jiàn)性問(wèn)題

但多線程中程序交錯(cuò)執(zhí)行時(shí)刹勃,重排序可能會(huì)造成內(nèi)存可見(jiàn)性問(wèn)題

可見(jiàn)性分析(多線程的情況):

image.png

?
執(zhí)行順序:

1.1->2.1->2.2—>1.2(線程的交叉執(zhí)行)
result的值為:3

1.2->2.1->2.2—>1.1(重排序結(jié)合線程交叉執(zhí)行)
result的值為:0

2.1和2.2之間也可以進(jìn)行重排序堪侯,因?yàn)橹灰皇菙?shù)據(jù)相互依存,都是不影響操作流程進(jìn)行重排序的荔仁。如圖:


image.png

?

導(dǎo)致共享變量在線程間不可見(jiàn)的原因:

1.線程的交叉執(zhí)行

2.重排序結(jié)合線程交叉執(zhí)行

3.共享變量更新后的值沒(méi)有在工作內(nèi)存與主內(nèi)存間及時(shí)更新

安全的代碼:(添加了sychronized)

?

synchronized解決方案:

1.線程的交叉執(zhí)行 ——> 原子性伍宦,獲得鎖對(duì)象,每次只能讓一個(gè)線程執(zhí)行鎖里面的代碼乏梁。

2.重排序結(jié)合線程交叉執(zhí)行 ——> 原子性雹拄。因?yàn)楂@得了鎖對(duì)象,并且在單線程中掌呜,無(wú)論如何進(jìn)行重排序滓玖,都不會(huì)影響最后的執(zhí)行結(jié)果。

3.共享變量未及時(shí)更新 ——> 可見(jiàn)性 通過(guò)synchronized的可見(jiàn)性規(guī)范的規(guī)定來(lái)保證的:1质蕉、加鎖前势篡,清空工作內(nèi)存中的共享變量,與主內(nèi)存中的共享變量進(jìn)行同步模暗。2禁悠、釋放鎖前:將工作內(nèi)存中的共享變量刷新到主內(nèi)存當(dāng)中。

上面的代碼通過(guò)synchronized以及它的兩條規(guī)范可以保證寫(xiě)操作和讀操作或按照預(yù)期情況進(jìn)行執(zhí)行兑宇,保證共享變量的可見(jiàn)性碍侦。

但反過(guò)來(lái)說(shuō),不加synchronized也并不是一定會(huì)導(dǎo)致共享變量不可見(jiàn)隶糕。

因?yàn)椴患?code>synchronized瓷产,共享變量也可能可以在工作內(nèi)存和主內(nèi)存中進(jìn)行同步。

這是因?yàn)榫幾g器做了一些優(yōu)化枚驻。它會(huì)去揣摩程序的意圖濒旦,試圖去給出一個(gè)正確的結(jié)果。因此可能程序運(yùn)行很多次才會(huì)出現(xiàn)一次共享變量在工作內(nèi)存和主內(nèi)存中更新不及時(shí)的情況再登,但可能就是因?yàn)檫@樣一次的情況尔邓,導(dǎo)致一些嚴(yán)重的后果。

?

volatile實(shí)現(xiàn)可見(jiàn)性

volatile關(guān)鍵字:

  • 能夠保證volatile變量的可見(jiàn)性

  • 不能保證volatile變量復(fù)合操作的原子性

volatile如何實(shí)現(xiàn)內(nèi)存可見(jiàn)性:

深入來(lái)說(shuō):通過(guò)加入內(nèi)存屏障禁止重排序優(yōu)化來(lái)實(shí)現(xiàn)的。

  • 對(duì)volatile變量執(zhí)行寫(xiě)操作時(shí),會(huì)在寫(xiě)操作后加入一條store屏障指令

??強(qiáng)制的將寫(xiě)緩存中的內(nèi)容強(qiáng)制刷新到主內(nèi)存當(dāng)中瓤球。并且還能防止將volatile之前的變量重排序到volatile寫(xiě)操作之后曙博。

  • 對(duì)volatile變量執(zhí)行讀操作時(shí),會(huì)在讀操作前加入一條load屏障指令

??強(qiáng)制使緩沖區(qū)的緩存失效,使得讀取volatile變量的時(shí)候,必須從主內(nèi)存中進(jìn)行讀取苹粟,同樣也能起到禁止指令重拍序的效果显晶。

通俗地講:volatile變量在每次被線程訪問(wèn)時(shí)贷岸,都強(qiáng)迫從主內(nèi)存中重讀該變量的值。

而當(dāng)該變量發(fā)生變化時(shí)磷雇,又會(huì)強(qiáng)迫線程將最新的值刷新到主內(nèi)存偿警。這樣任何時(shí)刻,不同的線程總能看到該變量的最新值唯笙。

?

線程寫(xiě)volatile變量的過(guò)程:

1.改變線程工作內(nèi)存中volatile變量副本的值

2.將改變后的副本的值從工作內(nèi)存刷新到主內(nèi)存

?

線程讀volatile變量的過(guò)程:

1.從主內(nèi)存中讀取volatile變量的最新值到線程的工作內(nèi)存中

2.從工作內(nèi)存中讀取volatile變量的副本

?

volatile不能保證volatile變量復(fù)合操作的原子性:

private int number=0螟蒸;

number++;

上面的操作不是原子操作崩掘,因?yàn)?code>number++可以分為三個(gè)步驟:

1.讀取number的值

2.將number的值加1

3.寫(xiě)入最新的number的值

?

加入synchronized七嫌,變?yōu)樵硬僮?/h5>
synchronized(this){

number++;

這是因?yàn)?code>synchronized會(huì)讓number++的分解出來(lái)的三個(gè)步驟被一個(gè)線程執(zhí)行完成以后苞慢,才會(huì)被另外一個(gè)線程去執(zhí)行诵原。而不會(huì)產(chǎn)生有幾個(gè)線程同時(shí)交叉去執(zhí)行這個(gè)三個(gè)步驟。

而把number變?yōu)関olatile變量挽放,無(wú)法保證原子性
private volatile int number=0绍赛;

假設(shè)此時(shí)number=5,線程 A 讀取number的值辑畦,當(dāng)線程 A 讀取完畢以后吗蚌,它的 CPU 使用權(quán)被剝奪了,從而導(dǎo)致 A 線程在讀取 number 的過(guò)程被阻塞在這里了纯出。

當(dāng) A 線程被阻塞的時(shí)候蚯妇,B 線程得到了執(zhí)行的機(jī)會(huì),它從主內(nèi)存中讀取 number 的值暂筝,并且對(duì) numebr 的值進(jìn)行加1箩言,并且將改變的值刷新到主內(nèi)存當(dāng)中。此時(shí) B 線程的工作內(nèi)存和主內(nèi)存中 number 的值均為 6乖杠。

而線程 A 的工作內(nèi)存中 number 的值還是5分扎,然后它也在經(jīng)過(guò):對(duì) numebr 的值進(jìn)行加1澄成,并且將改變的值刷新到主內(nèi)存當(dāng)中的操作胧洒。

此時(shí)經(jīng)過(guò)兩次 number++ 的操作,但線程 A 和線程 B 還有主內(nèi)存中 number 的值還是為 6墨状,從而導(dǎo)致實(shí)際輸出與預(yù)想輸出不一致的問(wèn)題卫漫。

這個(gè)問(wèn)題產(chǎn)生的原因是因?yàn)?volatile 不能保證操作的原子性。

解決方案:保證 number 自增操作的原子性:
  • 使用synchronized關(guān)鍵字

  • 使用ReentrantLock
    (java.until.concurrent.locks包下)

  • 使用AtomicInterger
    (vava.util.concurrent.atomic包下)

volatile適用場(chǎng)合

要在多線程中安全的使用volatile變量肾砂,必須同時(shí)滿(mǎn)足:

1.對(duì)變量的寫(xiě)入操作不依賴(lài)其當(dāng)前值

?不滿(mǎn)足:number++列赎、count=count*5等

?滿(mǎn)足:boolean變量、記錄溫度變化的變量等

2.該變量沒(méi)有包含在具有其他變量的不變式中

?不滿(mǎn)足:不變式 low

?

synchronized和volatile比較·

  • volatile不需要加鎖镐确,比synchronized更輕量級(jí)包吝,不會(huì)阻塞線程饼煞,volatile 執(zhí)行效率會(huì)更高。

  • 從內(nèi)存可見(jiàn)性角度將诗越,volatile讀相當(dāng)于加鎖(會(huì)將工作內(nèi)存和主內(nèi)存中的共享變量進(jìn)行同步)砖瞧,volatile寫(xiě)相當(dāng)于解鎖(會(huì)將工作內(nèi)存中的共享變量同步主內(nèi)存當(dāng)中)。

  • synchronized既能保證可見(jiàn)性嚷狞,又能保證原子性块促,而volatile只能保證可見(jiàn)性,無(wú)法保證原子性床未。

?

總結(jié)

  • 什么是內(nèi)存可見(jiàn)性

  • Java內(nèi)存模型(JMM)

  • 實(shí)現(xiàn)可見(jiàn)性的方式:

synchronized 和 volatile

?final也可以保證內(nèi)存可見(jiàn)性竭翠,因?yàn)樽兞勘欢x為 final 以后是不能被修改的

  • synchronized和volatile實(shí)現(xiàn)內(nèi)存可見(jiàn)性的原理

  • synchronized實(shí)現(xiàn)可見(jiàn)性

?指令重排序

?as-if-serial語(yǔ)義

  • volatile實(shí)現(xiàn)可見(jiàn)性(通過(guò) number++ 來(lái)舉例)

?volatile能夠保證可見(jiàn)性

?volatile不能保證原子性

  • 問(wèn):即使沒(méi)有保證可見(jiàn)性的措施,很多時(shí)候共享變量依然能夠在主內(nèi)存和工作內(nèi)存見(jiàn)得到及時(shí)的更新薇搁?

?答:一般只有在短時(shí)間內(nèi)高并發(fā)的情況下(線程之間相互交錯(cuò)執(zhí)行)才會(huì)出現(xiàn)變量得不到及時(shí)更新的情況斋扰,因?yàn)镃PU在執(zhí)行時(shí)會(huì)很快地刷新緩存,所以一般情況下很難看到這種問(wèn)題啃洋。這種情況的出現(xiàn)是隨機(jī)褥实、不可預(yù)測(cè)的。

  • 對(duì)64位(long裂允、double)變量的讀寫(xiě)可能不是原子操作:

?Java內(nèi)存模型允許JVM將沒(méi)有被volatile修飾的64位數(shù)據(jù)類(lèi)型的讀寫(xiě)操作劃分為兩次32位的讀寫(xiě)操作來(lái)進(jìn)行

?導(dǎo)致問(wèn)題:有可能會(huì)出現(xiàn)讀取到“半個(gè)變量”的情況

?解決方法:加volatile關(guān)鍵字

  • synchronized和volatile比較

?volatile比synchronized更輕量級(jí)

?volatile沒(méi)有synchronized使用的廣泛

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末损离,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子绝编,更是在濱河造成了極大的恐慌僻澎,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件十饥,死亡現(xiàn)場(chǎng)離奇詭異窟勃,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)逗堵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)秉氧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人蜒秤,你說(shuō)我怎么就攤上這事汁咏。” “怎么了作媚?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵攘滩,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我纸泡,道長(zhǎng)漂问,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮蚤假,結(jié)果婚禮上栏饮,老公的妹妹穿的比我還像新娘。我一直安慰自己磷仰,他們只是感情好抡爹,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著芒划,像睡著了一般冬竟。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上民逼,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天泵殴,我揣著相機(jī)與錄音,去河邊找鬼拼苍。 笑死笑诅,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的疮鲫。 我是一名探鬼主播吆你,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼俊犯!你這毒婦竟也來(lái)了妇多?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤燕侠,失蹤者是張志新(化名)和其女友劉穎者祖,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體绢彤,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡七问,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了茫舶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片械巡。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖饶氏,靈堂內(nèi)的尸體忽然破棺而出讥耗,到底是詐尸還是另有隱情,我是刑警寧澤嚷往,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布葛账,位于F島的核電站,受9級(jí)特大地震影響皮仁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一贷祈、第九天 我趴在偏房一處隱蔽的房頂上張望趋急。 院中可真熱鬧,春花似錦势誊、人聲如沸呜达。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)查近。三九已至,卻和暖如春挤忙,著一層夾襖步出監(jiān)牢的瞬間霜威,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工册烈, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留戈泼,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓赏僧,卻偏偏與公主長(zhǎng)得像大猛,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子淀零,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容