自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

99%的人沒弄懂Volatile的設(shè)計(jì)原理,更別說靈活運(yùn)用了

開發(fā) 架構(gòu)
最初的CPU是沒有緩存區(qū)的,CPU直接讀寫內(nèi)存。但這就存在一個(gè)問題,CPU的運(yùn)行效率與讀寫內(nèi)存的效率差距百倍以上??偛荒蹸PU執(zhí)行1個(gè)寫操作耗時(shí)1個(gè)時(shí)鐘周期,然后再等待內(nèi)存執(zhí)行一百多個(gè)時(shí)鐘周期吧。

[[427372]]

本文轉(zhuǎn)載自微信公眾號「程序新視界」,作者二師兄。轉(zhuǎn)載本文請聯(lián)系程序新視界公眾號。

寫volatile的文章非常多,本人也看過許多相關(guān)文章,但始終感覺有哪里不太明白,但又說不上來說為什么??赡苁沁^于追求底層實(shí)現(xiàn)原理,老想問一個(gè)為什么吧。

而寫這篇文章的目的很簡單,就是突然之間明白了volatile為什么要這樣設(shè)計(jì)了。好東西當(dāng)然要拿出來分享了,于是就有了這篇文章。

我們就從硬件到軟件,再到具體的案例來聊聊volatile的底層原理,文章比較長,可收藏之后閱讀。

CPU緩存的出現(xiàn)

最初的CPU是沒有緩存區(qū)的,CPU直接讀寫內(nèi)存。但這就存在一個(gè)問題,CPU的運(yùn)行效率與讀寫內(nèi)存的效率差距百倍以上??偛荒蹸PU執(zhí)行1個(gè)寫操作耗時(shí)1個(gè)時(shí)鐘周期,然后再等待內(nèi)存執(zhí)行一百多個(gè)時(shí)鐘周期吧。

于是在CPU和內(nèi)存之間添加了緩存(CPU緩存:Cache Memory),它是位于CPU和內(nèi)存之間的臨時(shí)存儲器。這就像當(dāng)Mysql出現(xiàn)瓶頸時(shí),我們會(huì)考慮通過緩存數(shù)據(jù)來提升性能一樣??傊?,CPU緩存的出現(xiàn)就是為了解決CPU和內(nèi)存之間處理速度不匹配的問題而誕生的。

這時(shí),我們有一個(gè)粗略的圖:

CPU-CPU緩存-內(nèi)存

但考慮到進(jìn)一步優(yōu)化數(shù)據(jù)的調(diào)度,CPU緩存又分為一級緩存、二級緩存、三級緩存等。它們主要用于優(yōu)化數(shù)據(jù)的吞吐和暫存,提高執(zhí)行效率。

目前主流CPU通常采用三層緩存:

  • 一級緩存(L1 Cache):主要用于緩存指令(L1P)和緩存數(shù)據(jù)(L1D),指令和數(shù)據(jù)是分開存儲的。一級緩存屬于核心獨(dú)享,比如4核電腦,則有4個(gè)L1。
  • 二級緩存(L2 Cache):二級緩存的指令和數(shù)據(jù)是共享的,二級緩存的容量會(huì)直接影響CPU的性能,越大越好。二級緩存同樣屬于核心獨(dú)享,4核心電腦,則有4個(gè)L2。
  • 三級緩存(L3 Cache):作用是進(jìn)一步降低內(nèi)存的延遲,同時(shí)提升海量數(shù)據(jù)計(jì)算的性能。三級緩存屬于核心共享的,因此只有1個(gè)。

經(jīng)過上述細(xì)分,可以將上圖進(jìn)一步細(xì)化:

CPU三級緩存

這里再補(bǔ)充一個(gè)概念:緩存行(Cache-line),它是CPU緩存存儲數(shù)據(jù)的最小單位,后面會(huì)用到。上面的CPU緩存,也稱作高速緩存。

引入緩存之后,每個(gè)CPU的處理過程為:先將計(jì)算所需數(shù)據(jù)緩存在高速緩存中,當(dāng)CPU進(jìn)行計(jì)算時(shí),直接從高速緩存讀取數(shù)據(jù),計(jì)算完成再寫入緩存中。當(dāng)整個(gè)運(yùn)算過程完成之后,再把緩存中的數(shù)據(jù)同步到主內(nèi)存中。

如果是單核CPU這樣處理沒有什么問題。但在多核系統(tǒng)中,每個(gè)CPU都可能將同一份數(shù)據(jù)緩存到自己的高速緩存中,這就出現(xiàn)了緩存數(shù)據(jù)一致性問題了。

CPU層提供了兩種解決方案:總線鎖和緩存一致性。

總線鎖

前端總線(也叫CPU總線)是所有CPU與芯片組連接的主干道,負(fù)責(zé)CPU與外界所有部件的通信,包括高速緩存、內(nèi)存、北橋,其控制總線向各個(gè)部件發(fā)送控制信號、通過地址總線發(fā)送地址信號指定其要訪問的部件、通過數(shù)據(jù)總線雙向傳輸。

比如CPU1要操作共享內(nèi)存數(shù)據(jù)時(shí),先在總線上發(fā)出一個(gè)LOCK#信號,其他處理器就不能操作緩存了該共享變量內(nèi)存地址的緩存,也就是阻塞了其他CPU,使該處理器可以獨(dú)享此共享內(nèi)存。

很顯然,這樣的做法代價(jià)十分昂貴,于是為了降低鎖粒度,CPU引入了緩存鎖。

緩存一致性協(xié)議

緩存一致性:緩存一致性機(jī)制整體來說,就是當(dāng)某塊CPU對緩存中的數(shù)據(jù)進(jìn)行操作了之后,會(huì)通知其他CPU放棄儲存在它們內(nèi)部的緩存,或者從主內(nèi)存中重新讀取。

緩存鎖的核心機(jī)制就是基于緩存一致性協(xié)議來實(shí)現(xiàn)的,即一個(gè)處理器的緩存回寫到內(nèi)存會(huì)導(dǎo)致其他處理器的緩存無效,IA-32處理器和Intel 64處理器使用MESI實(shí)現(xiàn)緩存一致性協(xié)議。

緩存一致性是一個(gè)協(xié)議,不同處理器的具體實(shí)現(xiàn)會(huì)有所不同,MESI是一種比較常見的緩存一致性協(xié)議實(shí)現(xiàn)。

MESI協(xié)議

MESI協(xié)議是以緩存行的幾個(gè)狀態(tài)來命名的(全名是Modified、Exclusive、Share or Invalid)。該協(xié)議要求在每個(gè)緩存行上維護(hù)兩個(gè)狀態(tài)位,每個(gè)數(shù)據(jù)單位可能處于M、E、S和I這四種狀態(tài)之一,各種狀態(tài)含義如下:

  • M(Modified):被修改的。該狀態(tài)的數(shù)據(jù),只在本CPU緩存中存在,其他CPU沒有。同時(shí),對于內(nèi)存中的值來說,是已經(jīng)被修改了,但還沒更新到內(nèi)存中去。也就是說緩存中的數(shù)據(jù)和內(nèi)存中的數(shù)據(jù)不一致。
  • E(Exclusive):獨(dú)占的。該狀態(tài)的數(shù)據(jù),只在本CPU緩存中存在,且并沒有被修改,與內(nèi)存數(shù)據(jù)一致。
  • S(Share):共享的。該狀態(tài)的數(shù)據(jù),在多個(gè)CPU緩存中同時(shí)存在,且與內(nèi)存數(shù)據(jù)一致。
  • I(Invalid):無效的。本CPU中的這份緩存數(shù)據(jù)已經(jīng)失效。

其中上述狀態(tài)隨著不同CPU的操作還會(huì)進(jìn)行不停的變更:

一個(gè)處于M狀態(tài)的緩存行,必須時(shí)刻監(jiān)聽所有試圖讀取該緩存行對應(yīng)的主存地址的操作,如果監(jiān)聽到,則必須在此操作執(zhí)行前把其緩存行中的數(shù)據(jù)寫回CPU。

一個(gè)處于S狀態(tài)的緩存行,必須時(shí)刻監(jiān)聽使該緩存行無效或者獨(dú)享該緩存行的請求,如果監(jiān)聽到,則必須把其緩存行狀態(tài)設(shè)置為I。

一個(gè)處于E狀態(tài)的緩存行,必須時(shí)刻監(jiān)聽其他試圖讀取該緩存行對應(yīng)的主存地址的操作,如果監(jiān)聽到,則必須把其緩存行狀態(tài)設(shè)置為S。

對于MESI協(xié)議,從CPU讀寫角度來說會(huì)遵循以下原則:

CPU讀數(shù)據(jù):當(dāng)CPU需要讀取數(shù)據(jù)時(shí),如果其緩存行的狀態(tài)是I的,則需要從內(nèi)存中讀取,并把自己狀態(tài)變成S,如果不是I,則可以直接讀取緩存中的值,但在此之前,必須要等待其他CPU的監(jiān)聽結(jié)果,如其他CPU也有該數(shù)據(jù)的緩存且狀態(tài)是M,則需要等待其把緩存更新到內(nèi)存之后,再讀取。

CPU寫數(shù)據(jù):當(dāng)CPU需要寫數(shù)據(jù)時(shí),只有在其緩存行是M或者E的時(shí)候才能執(zhí)行,否則需要發(fā)出特殊的RFO指令(Read Or Ownership,這是一種總線事務(wù)),通知其他CPU設(shè)置緩存無效(I),這種情況下性能開銷是相對較大的。在寫入完成后,修改其緩存狀態(tài)為M。

當(dāng)引入總線鎖或緩存一致性協(xié)議之后,CPU、緩存、內(nèi)存的結(jié)構(gòu)變?yōu)橄聢D:

CPU-緩存-總線-內(nèi)存

MESI協(xié)議帶來的問題

在上述MESI協(xié)議的交互過程中,我們已經(jīng)可以看到在各個(gè)CPU之間存在大量的消息傳遞(監(jiān)聽處理)。而緩存的一致性消息傳遞是需要時(shí)間的,這就使得切換時(shí)會(huì)產(chǎn)生延遲。一個(gè)CPU對緩存中數(shù)據(jù)的改變,可能需要獲得其他CPU的回執(zhí)之后才能繼續(xù)進(jìn)行,在這期間處于阻塞狀態(tài)。

Store Bufferes

等待確認(rèn)的過程會(huì)阻塞處理器,降低處理器的性能。而且這個(gè)等待遠(yuǎn)遠(yuǎn)比一個(gè)指令的執(zhí)行時(shí)間長的多。為了避免資源浪費(fèi),CPU又引入了存儲緩存(Store Bufferes)。

基于存儲緩存,CPU將要寫入內(nèi)存數(shù)據(jù)先寫入Store Bufferes中,同時(shí)發(fā)送消息,然后就可以繼續(xù)處理其他指令了。當(dāng)收到所有其他CPU的失效確認(rèn)(Invalidate Acknowledge)時(shí),數(shù)據(jù)才會(huì)最終被提交。

舉例說明一下Store Bufferes的執(zhí)行流程:比如將內(nèi)存中共享變量a的值由1修改為66。

第一步,CPU-0把a(bǔ)=66寫入Store Bufferes中,然后發(fā)送Invalid消息給其他CPU,無需等待其他CPU相應(yīng),便可繼續(xù)執(zhí)行其他指令了。

store bufferes

第二步,當(dāng)CPU-0收到其他所有CPU對Invalid通知的相應(yīng)之后,再把Store Bufferes中的共享變量同步到緩存和主內(nèi)存中。

store Bufferes

Store Forward(存儲轉(zhuǎn)發(fā))

Store Bufferes的引入提升了CPU的利用效率,但又帶來了新的問題。在上述第一步中,Store Bufferes中的數(shù)據(jù)還未同步到CPU-0自己的緩存中,如果此時(shí)CPU-0需要讀取該變量a,緩存中的數(shù)據(jù)并不是最新的,所以CPU需要先讀取Store Bufferes中是否有值。如果有則直接讀取,如果沒有再到自己緩存中讀取,這就是所謂的”Store Forward“。

失效隊(duì)列

CPU將數(shù)據(jù)寫入Store Bufferes的同時(shí)還會(huì)發(fā)消息給其他CPU,由于Store Bufferes空間較小,且其他CPU可能正在處理其他事情,沒辦法及時(shí)回復(fù),這個(gè)消息就會(huì)陷入等待。

為了避免接收消息的CPU無法及時(shí)處理Invalid失效數(shù)據(jù)的消息,造成CPU指令等待,就在接收CPU中添加了一個(gè)異步消息隊(duì)列。消息發(fā)送方將數(shù)據(jù)失效消息發(fā)送到這個(gè)隊(duì)列中,接收CPU返回已接收,發(fā)送方CPU就可以繼續(xù)執(zhí)行后續(xù)操作了。而接收方CPU再慢慢處理”失效隊(duì)列“中的消息。

內(nèi)存屏障

CPU經(jīng)過上述的一系列優(yōu)化,既保證了效率又確保了緩存的一致性,大多數(shù)情況下也是可以接受CPU基于Store Bufferes和失效隊(duì)列異步處理的短暫延遲的。

但在多線程的極端情況下,還是會(huì)產(chǎn)生緩存數(shù)據(jù)不一致的情況的。比如上述實(shí)例中,CPU-0修改數(shù)據(jù),發(fā)消息給其他CPU,其他CPU消息隊(duì)列接收成功并返回。這時(shí)CPU-1正忙著處理其他業(yè)務(wù),沒來得及處理消息隊(duì)列,而CPU-1處理的業(yè)務(wù)中恰好又用到了變量a,此時(shí)就會(huì)造成讀取到的a值為舊值。

這種因?yàn)镃PU緩存優(yōu)化導(dǎo)致后面的指令無法感知到前面指令的執(zhí)行結(jié)果,看起來就像指令之間的執(zhí)行順序錯(cuò)亂了一樣,對于這種現(xiàn)象我們俗稱“CPU亂序執(zhí)行”。

亂序執(zhí)行是導(dǎo)致多線程下程序Bug的原因,解決方案很簡單:禁用CPU緩存優(yōu)化。但大多數(shù)情況下的數(shù)據(jù)并不存在共享問題,直接禁用會(huì)導(dǎo)致整體性能下降,得不償失。于是就提供了針對多線程共享場景的解決機(jī)制:內(nèi)存屏障機(jī)制。

使用內(nèi)存屏障后,寫入數(shù)據(jù)時(shí)會(huì)保證所有指令都執(zhí)行完畢,這樣就能保證修改過的數(shù)據(jù)能夠即時(shí)暴露給其他CPU。而讀取數(shù)據(jù)時(shí),能夠保證所有“失效隊(duì)列”消息都消費(fèi)完畢。然后,CPU根據(jù)Invalid消息判斷自己緩存狀態(tài),正確讀寫數(shù)據(jù)。

CPU層面的內(nèi)存屏障

CPU層面提供了三類內(nèi)存屏障:

  • 寫屏障(Store Memory Barrier):告訴處理器在寫屏障之前將所有存儲在存儲緩存(store bufferes)中的數(shù)據(jù)同步到主內(nèi)存。也就是說當(dāng)看到Store Barrier指令,就必須把該指令之前所有寫入指令執(zhí)行完畢才能繼續(xù)往下執(zhí)行。
  • 讀屏障(Load Memory Barrier):處理器在讀屏障之后的讀操作,都在讀屏障之后執(zhí)行。也就是說在Load屏障指令之后就能夠保證后面的讀取數(shù)據(jù)指令一定能夠讀取到最新的數(shù)據(jù)。
  • 全屏障(Full Memory Barrier):兼具寫屏障和讀屏障的功能。確保屏障前的內(nèi)存讀寫操作的結(jié)果提交到內(nèi)存之后,再執(zhí)行屏障后的讀寫操作。

下面通過一段偽代碼來進(jìn)行說明:

  1. public class Demo { 
  2.     int value; 
  3.     boolean isFinish; 
  4.  
  5.     void cpu0(){ 
  6.         value = 10; // S->I狀態(tài),將value寫入store bufferes,通知其他CPU value緩存失效 
  7.         storeMemoryBarrier(); // 插入寫屏障,將value=10強(qiáng)制寫入主內(nèi)存 
  8.         isFinish = true; // E狀態(tài) 
  9.     } 
  10.      
  11.     void cpu1(){ 
  12.         if (isFinish){ // true 
  13.             loadMemoryBarrier(); //插入讀屏障,強(qiáng)制cpu1從主內(nèi)存中獲取最新數(shù)據(jù) 
  14.             System.out.println(value == 10); // true 
  15.         } 
  16.     } 
  17.  
  18.     void storeMemoryBarrier(){//寫屏障 
  19.     } 
  20.     void loadMemoryBarrier(){//讀屏障 
  21.     } 

上述實(shí)例中通過內(nèi)存屏障防止了指令重排,能夠得到預(yù)期的結(jié)果。

總之,內(nèi)存屏障的作用可以通過防止CPU亂序執(zhí)行來保證共享數(shù)據(jù)在多線程下的可見性。那么,在JVM中是如何解決該問題的呢?也就是編程人員如何進(jìn)行控制呢?這就涉及到我們要講的volatile關(guān)鍵字了。

Java內(nèi)存模型

內(nèi)存屏障解決了CPU緩存優(yōu)化導(dǎo)致的指令執(zhí)行的順序性和可見性問題,但不同的硬件系統(tǒng)提供的“內(nèi)存屏障”指令又有所不同,作為開發(fā)人員也沒必要熟悉所有的內(nèi)存屏障指令。而Java將不同的內(nèi)存屏障指令進(jìn)行了統(tǒng)一封裝,開發(fā)人員只需關(guān)注程序邏輯開發(fā)和內(nèi)存屏障規(guī)范即可。

這套封裝解決方案的模型就是我們常說的Java內(nèi)存模型(Java Memory Model),簡稱JMM。JMM最核心的價(jià)值便在于解決可見性和有序性,它是對硬件模型的抽象,定義了共享內(nèi)存中多線程程序讀寫操作的行為規(guī)范。

這套規(guī)范通過限定對內(nèi)存的讀寫操作從而保證指令的正確性,解決了CPU多級緩存、處理器優(yōu)化、指令重排序?qū)е碌膬?nèi)存訪問問題,保證了并發(fā)場景下的可見性。

本質(zhì)上,JMM是把硬件底層的問題抽象到了JVM層面,屏蔽了各個(gè)平臺的硬件差異,然后再基于CPU層面提供的內(nèi)存屏障指令以及限制編譯器的重排序來解決并發(fā)問題的。

JMM抽象模型結(jié)構(gòu)

JMM抽象模型中將內(nèi)存分為主內(nèi)存和工作內(nèi)存:

  • 主內(nèi)存:所有線程共享,存儲實(shí)例對象、靜態(tài)字段、數(shù)組對象等存儲在堆中的變量。
  • 工作內(nèi)存:每個(gè)線程獨(dú)享,線程對變量的所有操作都必須在工作內(nèi)存中進(jìn)行。

線程是CPU調(diào)度的最小單位,線程之間的共享變量值的傳遞都必須通過主內(nèi)存來完成。

JMM抽象模型結(jié)構(gòu)圖如下:

JMM抽象模型

JMM內(nèi)存模型簡單概述就是:

  • 所有變量存儲在主內(nèi)存;
  • 每條線程擁有自己的工作內(nèi)存,其中保存了主內(nèi)存中線程使用到的變量的副本;
  • 線程不能直接讀寫主內(nèi)存中的變量,所有操作均在工作內(nèi)存中完成;

如果線程A需要與線程B進(jìn)行通信,則線程A先把本地緩存中的數(shù)據(jù)更新到主內(nèi)存,再由線程B從主內(nèi)存中進(jìn)行獲取。JMM通過控制主內(nèi)存與每個(gè)線程的本地內(nèi)存之間的交互,來為Java程序提供內(nèi)存可見性保證。

編譯器指令重排

除了硬件層面的指令重排,Java編譯器為了提升性能,也會(huì)對指令進(jìn)行重排。Java規(guī)范規(guī)定JVM線程內(nèi)部維持順序化語義,即只要程序的最終結(jié)果與它順序化執(zhí)行的結(jié)果相等,那么指令的執(zhí)行順序可以與代碼順序不一致,此過程叫指令的重排序。

JVM能根據(jù)處理器特性(CPU多級緩存系統(tǒng)、多核處理器等)適當(dāng)?shù)膶C(jī)器指令進(jìn)行重排序,使機(jī)器指令能更符合CPU的執(zhí)行特性,最大限度的發(fā)揮機(jī)器性能。

從源碼到最終執(zhí)行示例圖:

指令重排序

其中2和3屬于CPU執(zhí)行階段的重排序,1屬于編譯器階段的重排序。編譯器會(huì)遵守happens-before規(guī)則和as-if-serial語義的前提下進(jìn)行指令重排。

happens-before規(guī)則:如果A happens-before B,且B happens-before C,則需要保證A happens-before C。

as-if-serial語義:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變。編譯器、Runtime和處理器都必須遵守as-if-serial語義。

對于處理器重排序,JMM要求Java編譯器在生成指令序列時(shí),插入特定類型的內(nèi)存屏障指令,來禁止特定類型的處理重排序。

JMM的內(nèi)存屏障

上面了解了CPU的內(nèi)存屏障分類,在JMM中把內(nèi)存屏障分為四類:

  • LoadLoad Barriers:示例,Load1;LoadLoad;Load2,確保Load1數(shù)據(jù)的裝載先于Load2及所有后續(xù)指令的裝載;
  • StoreStore Barriers:示例,Store1;StoreStore;Store2,確保Store1數(shù)據(jù)對其他處理器可見(刷新到內(nèi)存)先于Store2及所有后續(xù)存儲指令的存儲;
  • LoadStore Barriers:示例,Load1;LoadStore;Store2,確保Load1數(shù)據(jù)裝載先于Store2及所有后續(xù)存儲指令刷新到內(nèi)存;
  • StoreLoad Barriers:示例,Store1;StoreLoad;Load2,確保Store1數(shù)據(jù)對其他處理器變得可見(刷新到內(nèi)存)先于Load2及所有后續(xù)裝載指令的裝載。StoreLoad Barriers會(huì)使該屏障之前的所有內(nèi)存訪問指令(存儲和裝載指令)完成之后,才執(zhí)行該屏障之后的內(nèi)存訪問指令。

其中,StoreLoad Barriers同時(shí)具有前3個(gè)的屏障的效果,但性能開銷很大。

為了實(shí)現(xiàn)volatile內(nèi)存語義,JMM會(huì)分別限制這兩種類型的重排序類型。下圖是JMM針對編譯器制定的volatile重排序規(guī)則表。

JMM重排序

從圖中可以得出一個(gè)基本規(guī)則:

  • 當(dāng)?shù)诙€(gè)操作是volatile寫時(shí),不管第一個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保volatile寫之前的操作不會(huì)被編譯器重排序到volatile寫之后。
  • 當(dāng)?shù)谝粋€(gè)操作是volatile讀時(shí),不管第二個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保volatile讀之后的操作不會(huì)被編譯器重排序到volatile讀之前。
  • 當(dāng)?shù)谝粋€(gè)操作是volatile寫,第二個(gè)操作是volatile讀時(shí),不能重排序。

為了實(shí)現(xiàn)volatile的內(nèi)存語義,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。對于編譯器來說,發(fā)現(xiàn)一個(gè)最優(yōu)布置來最小化插入屏障的總數(shù)幾乎不可能。為此,JMM采取保守策略。下面是基于保守策略的JMM內(nèi)存屏障插入策略:

  • 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障。
  • 在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障。
  • 在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障。
  • 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障。

保守策略下volatile寫插入內(nèi)存屏障后生成的指令序列示意圖:

volatile寫屏障

保守策略下volatile讀插入內(nèi)存屏障后生成的指令序列示意圖:

volatile讀內(nèi)存屏障

JMM對volatile的特殊規(guī)則定義

JVM內(nèi)存指令與volatile相關(guān)的操作有:

  • read(讀取):作用于主內(nèi)存變量,把一個(gè)變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動(dòng)作使用;
  • load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中;
  • use(使用):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作;
  • assign(賦值):作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作;
  • store(存儲):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write的操作;
  • write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存的變量中;

在對volatile修飾的變量進(jìn)行操作時(shí),需滿足以下規(guī)則:

  • 規(guī)則1:線程對變量執(zhí)行的前一個(gè)動(dòng)作是load時(shí)才能執(zhí)行use,反之只有后一個(gè)動(dòng)作是use時(shí)才能執(zhí)行l(wèi)oad。線程對變量的read,load,use動(dòng)作關(guān)聯(lián),必須連續(xù)一起出現(xiàn)。這保證了線程每次使用變量時(shí)都需要從主存拿到最新的值,保證了其他線程修改的變量本線程能看到。
  • 規(guī)則2:線程對變量執(zhí)行的前一個(gè)動(dòng)作是assign時(shí)才能執(zhí)行store,反之只有后一個(gè)動(dòng)作是store時(shí)才能執(zhí)行assign。線程對變量的assign,store,write動(dòng)作關(guān)聯(lián),必須連續(xù)一起出現(xiàn)。這保證了線程每次修改變量后都會(huì)立即同步回主內(nèi)存,保證了本線程修改的變量其他線程能看到。
  • 規(guī)則3:有線程T,變量V、變量W。假設(shè)動(dòng)作A是T對V的use或assign動(dòng)作,P是根據(jù)規(guī)則2、3與A關(guān)聯(lián)的read或write動(dòng)作;動(dòng)作B是T對W的use或assign動(dòng)作,Q是根據(jù)規(guī)則2、3與B關(guān)聯(lián)的read或write動(dòng)作。如果A先與B,那么P先與Q。這保證了volatile修飾的變量不會(huì)被指令重排序優(yōu)化,代碼的執(zhí)行順序與程序的順序相同。

volatile實(shí)例及分析

通過上面的分析關(guān)于volatile關(guān)鍵詞的來源,以及被它修飾的變量的可見性和有序性都從理論層面講解清楚了。下面看一個(gè)可見性的實(shí)例。

示例代碼如下:

  1. public class VolatileTest { 
  2.  
  3.  private boolean initFlag = false
  4.  
  5.  public static void main(String[] args) throws InterruptedException { 
  6.   VolatileTest sample = new VolatileTest(); 
  7.   Thread threadA = new Thread(sample::refresh, "threadA"); 
  8.  
  9.   Thread threadB = new Thread(sample::load"threadB"); 
  10.  
  11.   threadB.start(); 
  12.   Thread.sleep(2000); 
  13.   threadA.start(); 
  14.  } 
  15.  
  16.  public void refresh() { 
  17.   this.initFlag = true
  18.   System.out.println("線程:" + Thread.currentThread().getName() + ":修改共享變量initFlag"); 
  19.  } 
  20.  
  21.  public void load() { 
  22.   int i = 0; 
  23.   while (!initFlag) { 
  24.   } 
  25.   System.out.println("線程:" + Thread.currentThread().getName() + "當(dāng)前線程嗅探到initFlag的狀態(tài)的改變" + i); 
  26.  } 

根據(jù)上面的理論知識,先猜測一下線程先后打印出的內(nèi)容是什么?先打印”線程threadA修改共享變量initFlag“,然后打印”線程threadB當(dāng)前線程嗅探到initFlag的狀態(tài)的改變0“?

當(dāng)真正執(zhí)行程序時(shí),會(huì)發(fā)現(xiàn)整個(gè)線程阻塞在while循環(huán)處,并未打印出第2條內(nèi)容。此時(shí)JMM操作如下圖:

thread-without-volatile

雖然線程A中將initFlag改為了true并且最終會(huì)同步回主內(nèi)存,但是線程B中循環(huán)讀取的initFlag一直都是從工作內(nèi)存讀取的,所以會(huì)一直進(jìn)行死循環(huán)無法退出。

當(dāng)對變量initFlag添加了volatile修飾之后:

  1. public class VolatileTest { 
  2.  
  3.  private volatile boolean initFlag = false
  4.  //... 

JMM操作如下圖:

thread-with-volatile

添加了volatile修飾之后,兩句日志都會(huì)被打印出來。這是因?yàn)樘砑觱olatile關(guān)鍵字后,就會(huì)有l(wèi)ock指令,使用緩存一致性協(xié)議,線程B中會(huì)一直嗅探initFlag是否被改變,線程A修改initFlag后會(huì)立即同步回主內(nèi)存,同時(shí)通知線程B將緩存行狀態(tài)改為I(無效狀態(tài)),重新從主內(nèi)存讀取。

volatile無法保證原子性

volatile雖然保證了共享變量的可見性和有序性,但并不能夠保證原子性。

以常見的自增操作(count++)為例來進(jìn)行說明,通常自增操作底層是分三步的:

  • 第一步:獲取變量count;
  • 第二步:count加1;
  • 第三步:回寫count。

我們來分析一下在這個(gè)過程中會(huì)有的線程安全問題:

第一步,線程A和B同時(shí)獲得count的初始值,這一步?jīng)]什么問題;

第二步,線程A自增count并回寫,但線程B此時(shí)也已經(jīng)拿到count,不會(huì)再去拿線程A回寫的值,因此對原始值進(jìn)行自增并回寫,這就導(dǎo)致了線程安全的問題。有人可能要問了,線程A自增之后不是應(yīng)該通知其他CPU緩存失效嗎,并重新load嗎?我們要知道,重新獲取的前提操作是讀,在線程A回寫時(shí),線程B已經(jīng)拿到了count的值,并不存在再次讀的場景。也就是說,線程B的緩存行的確會(huì)失效,但線程B中count值已經(jīng)運(yùn)行在加法指令中,不存在需要再次從緩存行讀的場景。

volatile關(guān)鍵字只保證可見性,所以在以下情況中,需要使用鎖來保證原子性:

  • 運(yùn)算結(jié)果依賴變量的當(dāng)前值,并且有不止一個(gè)線程在修改變量的值。
  • 變量需要與其他狀態(tài)變量共同參與不變約束

所以,想要使用volatile變量提供理想的線程安全,必須同時(shí)滿足兩個(gè)條件:

  • 對變量的寫操作不依賴于當(dāng)前值。
  • 該變量沒有包含在具有其他變量的不變式中。

也就是說被修飾的變量值獨(dú)立于任何程序的狀態(tài),包括變量的當(dāng)前狀態(tài)。

volatile適用場景

狀態(tài)標(biāo)志

使用一個(gè)布爾狀態(tài)標(biāo)志,用于指示發(fā)生了一個(gè)重要的一次性事件,例如完成初始化或請求停機(jī)。

  1. volatile boolean shutdownRequested; 
  2.   
  3. ... 
  4.   
  5. public void shutdown() {  
  6.     shutdownRequested = true;  
  7.   
  8. public void doWork() {  
  9.     while (!shutdownRequested) {  
  10.         // do stuff 
  11.     } 

線程1執(zhí)行doWork()的過程中,線程2可能調(diào)用了shutdown,所以boolean變量必須是volatile。

這種狀態(tài)標(biāo)記的一個(gè)公共特性是:通常只有一種狀態(tài)轉(zhuǎn)換;shutdownRequested 標(biāo)志從false 轉(zhuǎn)換為true,然后程序停止。這種模式可以擴(kuò)展到來回轉(zhuǎn)換的狀態(tài)標(biāo)志,但是只有在轉(zhuǎn)換周期不被察覺的情況下才能擴(kuò)展(從false 到true,再轉(zhuǎn)換到false)。此外,還需要某些原子狀態(tài)轉(zhuǎn)換機(jī)制,例如原子變量。

一次性安全發(fā)布

在缺乏同步的情況下,可能會(huì)遇到某個(gè)對象引用的更新值(由另一個(gè)線程寫入)和該對象狀態(tài)的舊值同時(shí)存在。

這種場景在著名的雙重檢查鎖定(double-checked-locking)中會(huì)出現(xiàn):

  1. //注意volatile! 
  2. private volatile static Singleton instace;    
  3.    
  4. public static Singleton getInstance(){    
  5.     //第一次null檢查      
  6.     if(instance == null){             
  7.         synchronized(Singleton.class) {    //1      
  8.             //第二次null檢查        
  9.             if(instance == null){          //2   
  10.                 instance = new Singleton();//3   
  11.             }   
  12.         }            
  13.     }   
  14.     return instance;         

其中第3步中實(shí)例化Singleton分多步執(zhí)行(分配內(nèi)存空間、初始化對象、將對象指向分配的內(nèi)存空間),某些編譯器為了性能原因,會(huì)將第二步和第三步進(jìn)行重排序(分配內(nèi)存空間、將對象指向分配的內(nèi)存空間、初始化對象)。這樣,某個(gè)線程可能會(huì)獲得一個(gè)未完全初始化的實(shí)例。

獨(dú)立觀察(independent observation)

場景:定期 “發(fā)布” 觀察結(jié)果供程序內(nèi)部使用。比如,傳感器感知溫度,一個(gè)線程每隔幾秒讀取一次傳感器,并更新當(dāng)前的volatile修飾變量。其他線程可以讀取這個(gè)變量,隨時(shí)看到最新溫度。

另一種場景就是應(yīng)用程序搜集統(tǒng)計(jì)信息。比如記錄最后一次登錄的用戶名,反復(fù)使用lastUser引用來發(fā)布值,以供其他程序使用。

  1. public class UserManager { 
  2.     public volatile String lastUser; //發(fā)布的信息 
  3.   
  4.     public boolean authenticate(String user, String password) { 
  5.         boolean valid = passwordIsValid(userpassword); 
  6.         if (valid) { 
  7.             User u = new User(); 
  8.             activeUsers.add(u); 
  9.             lastUser = user
  10.         } 
  11.         return valid; 
  12.     } 
  13. }  

“volatile bean” 模式

volatile bean 模式的基本原理是:很多框架為易變數(shù)據(jù)的持有者(例如 HttpSession)提供了容器,但是放入這些容器中的對象必須是線程安全的。在 volatile bean 模式中,JavaBean 的所有數(shù)據(jù)成員都是 volatile 類型的,并且 getter 和 setter 方法必須非常普通——即不包含約束。

  1. @ThreadSafe 
  2. public class Person { 
  3.     private volatile String firstName; 
  4.     private volatile String lastName; 
  5.     private volatile int age; 
  6.   
  7.     public String getFirstName() { return firstName; } 
  8.     public String getLastName() { return lastName; } 
  9.     public int getAge() { return age; } 
  10.   
  11.     public void setFirstName(String firstName) {  
  12.         this.firstName = firstName; 
  13.     } 
  14.   
  15.     public void setLastName(String lastName) {  
  16.         this.lastName = lastName; 
  17.     } 
  18.   
  19.     public void setAge(int age) {  
  20.         this.age = age; 
  21.     } 

開銷較低的“讀-寫鎖”策略

如果讀操作遠(yuǎn)遠(yuǎn)超過寫操作,可以結(jié)合使用內(nèi)部鎖和 volatile 變量來減少公共代碼路徑的開銷。

如下線程安全的計(jì)數(shù)器代碼,使用 synchronized 確保增量操作是原子的,并使用 volatile 保證當(dāng)前結(jié)果的可見性。如果更新不頻繁的話,該方法可實(shí)現(xiàn)更好的性能,因?yàn)樽x路徑的開銷僅僅涉及 volatile 讀操作,這通常要優(yōu)于一個(gè)無競爭的鎖獲取的開銷。

  1. @ThreadSafe 
  2. public class CheesyCounter { 
  3.     // Employs the cheap read-write lock trick 
  4.     // All mutative operations MUST be done with the 'this' lock held 
  5.     @GuardedBy("this") private volatile int value; 
  6.   
  7.     //讀操作,沒有synchronized,提高性能 
  8.     public int getValue() {  
  9.         return value;  
  10.     }  
  11.   
  12.     //寫操作,必須synchronized。因?yàn)閤++不是原子操作 
  13.     public synchronized int increment() { 
  14.         return value++; 
  15.     } 

使用鎖進(jìn)行有變化的操作,使用volatile進(jìn)行只讀操作。volatile允許多個(gè)線程同時(shí)執(zhí)行讀操作。

小結(jié)

本文先從硬件層面分析CPU的處理機(jī)制,為了優(yōu)化CPU引入了緩存,為了更進(jìn)一步優(yōu)化引入了Store Bufferes,而Store Bufferes導(dǎo)致了緩存一致性問題。于是有了總線鎖和緩存一致性協(xié)議(EMSI實(shí)現(xiàn)),從而有了CPU的內(nèi)存屏障機(jī)制。

而CPU的內(nèi)存屏障反映在Java編程語言中,有了Java內(nèi)存模型(JMM),JMM屏蔽了底層硬件的不同,提供了統(tǒng)一的操作,進(jìn)而編程人員可以通過volatile關(guān)鍵字來解決共享變量的可見性和順序性。

緊接著,通過實(shí)例演示了volatile的作用以及它不具有線程安全保證的反面案例。最后,舉例說明volatile的運(yùn)用場景。

想必通過這篇文章,你已經(jīng)徹底弄懂了volatile相關(guān)的知識了吧?來,關(guān)注一波。

 

責(zé)任編輯:武曉燕 來源: 程序新視界
相關(guān)推薦

2009-12-07 17:20:29

PHP stdClas

2009-10-23 15:30:53

無線接入技術(shù)

2011-07-25 16:25:47

2019-10-21 15:30:54

JS技巧前端

2009-02-20 10:59:21

Vista幫助系統(tǒng)使用技巧

2010-04-21 14:56:23

Unix 線程

2010-04-27 17:06:16

AIX vmstat

2009-07-01 17:58:20

JSP

2011-07-08 13:56:00

域控制器服務(wù)器

2013-04-10 10:39:57

2013-04-07 10:15:34

2021-07-12 07:08:52

Spring Boot集成框架

2024-01-26 16:28:28

C++動(dòng)態(tài)內(nèi)存開發(fā)

2009-12-01 11:33:03

PHP判斷字符串的包含

2012-01-10 10:05:47

文件目錄訪問控制UGO

2012-02-04 14:56:52

JP1數(shù)據(jù)中心

2010-05-27 13:32:36

IIS服務(wù)安全認(rèn)證

2011-08-23 18:30:59

MySQLTIMESTAMP

2016-10-08 12:46:08

Linux監(jiān)控限制

2011-08-17 09:47:55

windows7搜索
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號