摘要:一接觸內(nèi)存模型中的實(shí)例靜態(tài)變量以及數(shù)組都存儲(chǔ)在堆內(nèi)存中,可在線(xiàn)程之間共享。所以,在編碼上實(shí)現(xiàn)鎖的內(nèi)存語(yǔ)義,可以通過(guò)對(duì)一個(gè)變量的讀寫(xiě),來(lái)實(shí)現(xiàn)線(xiàn)程之間相互通知,保證臨界區(qū)域代碼的互斥執(zhí)行。
原文發(fā)表于我的博客
volatile關(guān)鍵字: 使用volatile關(guān)鍵字修飾的的變量,總能“看到”任意線(xiàn)程對(duì)它最后的寫(xiě)入,即總能保證任意線(xiàn)程在讀寫(xiě)volatile修飾的變量時(shí),總是從內(nèi)存中讀取最新的值。以下是volatile在內(nèi)存中的語(yǔ)義實(shí)現(xiàn)及同步的原理。
Java中的實(shí)例、靜態(tài)變量以及數(shù)組都存儲(chǔ)在堆內(nèi)存中,可在線(xiàn)程之間共享。而Java進(jìn)程間通信由Java內(nèi)存模型(JMM)控制,JMM可以決定共享變量的寫(xiě)入何時(shí)對(duì)另一個(gè)線(xiàn)程可見(jiàn)。(從JDK5開(kāi)始,Java使用JSR-133內(nèi)存模型,從該規(guī)定開(kāi)始,即使是在32位的機(jī)器上,一個(gè)64位的double/long的讀操作也必須滿(mǎn)足原子性)
[圖1.1]
本地內(nèi)存是JMM抽象的一個(gè)概念
從我學(xué)習(xí)編程語(yǔ)言開(kāi)始,所認(rèn)知的是“程序順序執(zhí)行”。然而,順序一致性只是一種理想模型。從源代碼到機(jī)器指令的這一過(guò)程中,編譯器和處理器往往會(huì)對(duì)指令做一些重排序從而提高性能,但是重排序會(huì)依據(jù)一個(gè)標(biāo)準(zhǔn):
不改變單線(xiàn)程程序語(yǔ)義
不影響數(shù)據(jù)依賴(lài)。
如果一個(gè)操作的執(zhí)行結(jié)果需要對(duì)另一個(gè)操作可見(jiàn),則兩個(gè)操作之間滿(mǎn)足happens-before關(guān)系。happens-before具有傳遞性
對(duì)于一個(gè)volatile變量的寫(xiě)操作,happens-before于任意后續(xù)對(duì)這個(gè)變量的讀
as-if-serial規(guī)定,如果操作直接存在數(shù)據(jù)依賴(lài)關(guān)系,則不允許重排序。不管怎么重排序,都必須遵守as-if-serial語(yǔ)義。
int a = 1; //(1) int b = 2; //(2) int c = a + b; //(3)
上面的代碼中,(1)(2)之間不存在以來(lái)和happens-before關(guān)系,可以重排序,而(1)(3)和(2)(3)之間都存在as-if-serial關(guān)系,不能重排序
as-if-serial保護(hù)單線(xiàn)程程程序的語(yǔ)義正確性,使我們無(wú)需擔(dān)心重排序?qū)ξ覀兊挠绊?,也使我們產(chǎn)生一種錯(cuò)覺(jué):?jiǎn)尉€(xiàn)程程序就是順序執(zhí)行的。
拓展資料--重排序的三種類(lèi)型:
(1)編譯器優(yōu)化重排序
(2)指令集并行重排序
(3)內(nèi)存系統(tǒng)的重排序
我們將happens-before和as-if-serial的關(guān)系引入到多線(xiàn)程中。我們可以將多線(xiàn)程的所有操作想象成在時(shí)間軸上的順序執(zhí)行的單線(xiàn)程程序。(以下流程圖使用Markdown語(yǔ)法繪制,有些地方不支持)
在多線(xiàn)程的程序中,假如線(xiàn)程相互之間不涉及共享的變量,亦即互相不干涉,則兩個(gè)線(xiàn)程之間既沒(méi)有happens-before的關(guān)系,也沒(méi)有as-if-serial語(yǔ)義的約束,所以各個(gè)線(xiàn)程之間操作可以任意合并重排序:
線(xiàn)程A的執(zhí)行流程
st=>start: 線(xiàn)程A op1=>operation: op-a-1 op2=>operation: op-a-2 op3=>operation: op-a-3 e=>end st->op1->op2->op3->e
線(xiàn)程B的執(zhí)行流程
st=>start: 線(xiàn)程B op1=>operation: op-b-1 op2=>operation: op-b-2 op3=>operation: op-b-3 e=>end st->op1->op2->op3->e
并發(fā)的可能的執(zhí)行順序
st=>start: 重排序 op1=>operation: op-a-1 op2=>operation: op-b-1 op3=>operation: op-b-2 op4=>operation: op-a-2 op5=>operation: op-a-3 op6=>operation: op-b-3 e=>end st->op1->op2->op3->op4->op5->op6->e
當(dāng)線(xiàn)程之間涉及到共享變量時(shí),涉及到了線(xiàn)程之間的通信,即如圖1.1所示,此時(shí)并發(fā)所存在的問(wèn)題(臟讀、幻讀、不可重復(fù)讀)明顯可見(jiàn),但是,如果線(xiàn)程沒(méi)有正確地同步(通信),線(xiàn)程之間無(wú)法明確共享變量何時(shí)被寫(xiě)入。因?yàn)榇藭r(shí)所面對(duì)的問(wèn)題就如將線(xiàn)程合并到時(shí)間軸上和重排序后是否違反happens-before和as-if-serial的語(yǔ)義了:
線(xiàn)程A
st=>start: 線(xiàn)程A op1=>operation: a讀共享變量x op2=>operation: a寫(xiě)共享變量x e=>end st->op1->op2->e
線(xiàn)程B
st=>start: 線(xiàn)程B op1=>operation: b讀共享變量x op2=>operation: b寫(xiě)共享變量x e=>end st->op1->op2->e
假如不同步,程序可能的執(zhí)行順序
st=>start: 重排序 op1=>operation: a讀共享變量x op2=>operation: b寫(xiě)共享變量x op3=>operation: b讀共享變量x op4=>operation: a寫(xiě)共享變量x e=>end st->op1->op2->3->4->e
上面的程序執(zhí)行順序很顯然有臟讀的問(wèn)題,而程序并發(fā)執(zhí)行的正確語(yǔ)義應(yīng)該有如下兩種:
a讀共享變量x happens-before a寫(xiě)共享變量x,a寫(xiě)共享變量x happens-before b讀共享變量x, b讀共享變量x happens-before b寫(xiě)共享變量x
b讀共享變量x happens-before b寫(xiě)共享變量x,b寫(xiě)共享變量x happens-before a讀共享變量x,a讀共享變量x happens-before a寫(xiě)共享變量x
所以,為程序保證并發(fā)操作的正確性,多線(xiàn)程對(duì)共享變量的非原子操作上,必須采用有效的通信方式來(lái)使其對(duì)共享變量的操作對(duì)其它線(xiàn)程可見(jiàn),這就引入了volatile同步方式。
JMM通過(guò)在指令序列中插入內(nèi)存屏障來(lái)限制編譯器的指令重排序,實(shí)現(xiàn)volatile的內(nèi)存語(yǔ)義
普通讀
普通寫(xiě)
StoreStore屏障:禁止前面的普通寫(xiě)和volatile寫(xiě)重排序,保證前面的普通變量寫(xiě)從本地內(nèi)存緩存刷新到主存中
volatile寫(xiě)
StoreLoad屏障:防止volatile寫(xiě)和下面有可能出現(xiàn)的volatile讀發(fā)生重排序
volatile讀
LoadLoad屏障:禁止下面的普通讀和volatile讀重排序
LoadStore屏障:禁止下面的普通寫(xiě)和volatile讀重排序
從上面我們可以看到,通過(guò)volatile關(guān)鍵字,構(gòu)建了happens-before的關(guān)系,限制普通變量和volatile變量讀寫(xiě)的操作指令重排序,有效保證了程序語(yǔ)義的正確性。從下面圖我們可以進(jìn)一步分析:
[圖4.2 volatile使線(xiàn)程之間的對(duì)共享變量操作的同步]
所以,volatile變量的寫(xiě)-讀操作語(yǔ)義和Lock的獲取-釋放語(yǔ)義相同(這是JSR-133對(duì)volatile內(nèi)存語(yǔ)義增強(qiáng)后的),使用volatile我們亦可以靈活、輕量地實(shí)現(xiàn)對(duì)共享(普通)變量的同步:
volatile [volatile static int a = 1] | Lock |
---|---|
讀volatile變量:while(a!=1); | Lock acquire() |
操作臨界資源(共享變量) | 操作臨界資源(共享變量) |
寫(xiě)volatile變量[a=1] | Lock release() |
寫(xiě)volatile變量時(shí),會(huì)將共享變量的本地內(nèi)存中的修改刷新到主存中
要想使用volatile完全代替鎖還需謹(jǐn)慎,volatile比較難像鎖一樣可以很好地保證整個(gè)臨界區(qū)域代碼的原子性
vloatile 保證對(duì)單個(gè)volatile變量讀/寫(xiě)的原子性
鎖 保證臨界區(qū)域互斥執(zhí)行
但是,volatile的內(nèi)存語(yǔ)義為我們提供了鎖的思路,正如上面表格中使用volatile模仿Lock進(jìn)行同步,既保證了臨界區(qū)域的互斥執(zhí)行,又保證了任意線(xiàn)程對(duì)共享變量修改及時(shí)刷新到主內(nèi)存中,保證了線(xiàn)程間有效通信從而避免并發(fā)操作臨界資源的一些問(wèn)題。
鎖可以讓臨界區(qū)域互斥執(zhí)行,那么線(xiàn)程之間必然存在一個(gè)同步的機(jī)制。
(1)volatile讀 -----> |屏障|--->臨界操作--->|屏障|--->volatile寫(xiě),成對(duì)的volatile構(gòu)建了happens-before的關(guān)系,并且保證了普通共享變量在volatile寫(xiě)之前刷新到主內(nèi)存中
(2)結(jié)合線(xiàn)程間通信的方式
[圖4.3 線(xiàn)程間通信的方式]
上圖的線(xiàn)程A對(duì)共享變量的寫(xiě)和線(xiàn)程B的讀共享變量,有單線(xiàn)程程序的順序一致性效果,此時(shí)我們可以想到volatile的作用。通過(guò)volatile變量,可以實(shí)現(xiàn)從A線(xiàn)程發(fā)送通知到B線(xiàn)程并且能夠保證happens-before的語(yǔ)義正確性(在并發(fā)時(shí)就很好理解為什么happens-before并不要求前一個(gè)操作一定要在后一個(gè)操作之前執(zhí)行,只需要前一個(gè)操作的結(jié)果對(duì)后一個(gè)操作可見(jiàn)),此時(shí)我們可推出鎖獲取和釋放的內(nèi)存語(yǔ)義:
線(xiàn)程A釋放一個(gè)鎖,即線(xiàn)程A向接下來(lái)要獲取鎖的線(xiàn)程B發(fā)出消息(A修改共享的變量)
線(xiàn)程B獲取一個(gè)鎖,即線(xiàn)程B接收到之前某個(gè)線(xiàn)程發(fā)出的消息(共享變量發(fā)生變化)
從一個(gè)線(xiàn)程釋放鎖,到另一個(gè)線(xiàn)程釋放鎖,實(shí)際上是兩條線(xiàn)程通過(guò)主線(xiàn)程同步對(duì)共享變量的操作,通過(guò)主內(nèi)存相互通信。所以,在Java編碼上實(shí)現(xiàn)鎖的內(nèi)存語(yǔ)義,可以通過(guò)對(duì)一個(gè)volatile變量的讀寫(xiě),來(lái)實(shí)現(xiàn)線(xiàn)程之間相互通知,保證臨界區(qū)域代碼的互斥執(zhí)行。
[以上內(nèi)容參考了《Java并發(fā)編程的藝術(shù)》,可能有謬誤,歡迎指正]
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/69781.html
摘要:前情提要深入理解內(nèi)存模型三順序一致性的特性當(dāng)我們聲明共享變量為后,對(duì)這個(gè)變量的讀寫(xiě)將會(huì)很特別。當(dāng)讀線(xiàn)程的數(shù)量大大超過(guò)寫(xiě)線(xiàn)程時(shí),選擇在寫(xiě)之后插入屏障將帶來(lái)可觀(guān)的執(zhí)行效率的提升。 前情提要 深入理解Java內(nèi)存模型(三)——順序一致性 volatile的特性 當(dāng)我們聲明共享變量為volatile后,對(duì)這個(gè)變量的讀/寫(xiě)將會(huì)很特別。理解volatile特性的一個(gè)好方法是:把對(duì)volatil...
摘要:內(nèi)存模型對(duì)內(nèi)存模型的介紹對(duì)內(nèi)存模型的結(jié)構(gòu)圖的線(xiàn)程之間的通信是通過(guò)共享內(nèi)存的方式進(jìn)行隱式通信,即線(xiàn)程把某狀態(tài)寫(xiě)入主內(nèi)存中的共享變量,線(xiàn)程讀取的值,這樣就完成了通信。 Java內(nèi)存模型(JMM) 1.對(duì)內(nèi)存模型的介紹 ①對(duì)Java內(nèi)存模型的結(jié)構(gòu)圖 java的線(xiàn)程之間的通信是通過(guò)共享內(nèi)存的方式進(jìn)行隱式通信,即線(xiàn)程A把某狀態(tài)寫(xiě)入主內(nèi)存中的共享變量X,線(xiàn)程B讀取X的值,這樣就完成了通信。是一種...
摘要:并發(fā)編程的挑戰(zhàn)并發(fā)編程的目的是為了讓程序運(yùn)行的更快,但是,并不是啟動(dòng)更多的線(xiàn)程就能讓程序最大限度的并發(fā)執(zhí)行。的實(shí)現(xiàn)原理與應(yīng)用在多線(xiàn)程并發(fā)編程中一直是元老級(jí)角色,很多人都會(huì)稱(chēng)呼它為重量級(jí)鎖。 并發(fā)編程的挑戰(zhàn) 并發(fā)編程的目的是為了讓程序運(yùn)行的更快,但是,并不是啟動(dòng)更多的線(xiàn)程就能讓程序最大限度的并發(fā)執(zhí)行。如果希望通過(guò)多線(xiàn)程執(zhí)行任務(wù)讓程序運(yùn)行的更快,會(huì)面臨非常多的挑戰(zhàn):(1)上下文切換(2)死...
摘要:前半句是指線(xiàn)程內(nèi)表現(xiàn)為串行的語(yǔ)義,后半句是指指令重排序現(xiàn)象和工作內(nèi)存和主內(nèi)存同步延遲現(xiàn)象。關(guān)于內(nèi)存模型的講解請(qǐng)參考死磕同步系列之。目前國(guó)內(nèi)市面上的關(guān)于內(nèi)存屏障的講解基本不會(huì)超過(guò)這三篇文章,包括相關(guān)書(shū)籍中的介紹。問(wèn)題 (1)volatile是如何保證可見(jiàn)性的? (2)volatile是如何禁止重排序的? (3)volatile的實(shí)現(xiàn)原理? (4)volatile的缺陷? 簡(jiǎn)介 volatile...
摘要:前半句是指線(xiàn)程內(nèi)表現(xiàn)為串行的語(yǔ)義,后半句是指指令重排序現(xiàn)象和工作內(nèi)存和主內(nèi)存同步延遲現(xiàn)象。關(guān)于內(nèi)存模型的講解請(qǐng)參考死磕同步系列之。目前國(guó)內(nèi)市面上的關(guān)于內(nèi)存屏障的講解基本不會(huì)超過(guò)這三篇文章,包括相關(guān)書(shū)籍中的介紹。問(wèn)題 (1)volatile是如何保證可見(jiàn)性的? (2)volatile是如何禁止重排序的? (3)volatile的實(shí)現(xiàn)原理? (4)volatile的缺陷? 簡(jiǎn)介 volatile...
閱讀 1128·2021-11-24 09:39
閱讀 1431·2021-11-18 13:18
閱讀 2578·2021-11-15 11:38
閱讀 1896·2021-09-26 09:47
閱讀 1714·2021-09-22 15:09
閱讀 1691·2021-09-03 10:29
閱讀 1593·2019-08-29 17:28
閱讀 3013·2019-08-29 16:30