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