成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

99%的人沒弄懂Volatile的設計原理,更別說靈活運用了

開發 架構
最初的CPU是沒有緩存區的,CPU直接讀寫內存。但這就存在一個問題,CPU的運行效率與讀寫內存的效率差距百倍以上??偛荒蹸PU執行1個寫操作耗時1個時鐘周期,然后再等待內存執行一百多個時鐘周期吧。

[[427372]]

本文轉載自微信公眾號「程序新視界」,作者二師兄。轉載本文請聯系程序新視界公眾號。

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

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

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

CPU緩存的出現

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

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

這時,我們有一個粗略的圖:

CPU-CPU緩存-內存

但考慮到進一步優化數據的調度,CPU緩存又分為一級緩存、二級緩存、三級緩存等。它們主要用于優化數據的吞吐和暫存,提高執行效率。

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

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

經過上述細分,可以將上圖進一步細化:

CPU三級緩存

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

引入緩存之后,每個CPU的處理過程為:先將計算所需數據緩存在高速緩存中,當CPU進行計算時,直接從高速緩存讀取數據,計算完成再寫入緩存中。當整個運算過程完成之后,再把緩存中的數據同步到主內存中。

如果是單核CPU這樣處理沒有什么問題。但在多核系統中,每個CPU都可能將同一份數據緩存到自己的高速緩存中,這就出現了緩存數據一致性問題了。

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

總線鎖

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

比如CPU1要操作共享內存數據時,先在總線上發出一個LOCK#信號,其他處理器就不能操作緩存了該共享變量內存地址的緩存,也就是阻塞了其他CPU,使該處理器可以獨享此共享內存。

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

緩存一致性協議

緩存一致性:緩存一致性機制整體來說,就是當某塊CPU對緩存中的數據進行操作了之后,會通知其他CPU放棄儲存在它們內部的緩存,或者從主內存中重新讀取。

緩存鎖的核心機制就是基于緩存一致性協議來實現的,即一個處理器的緩存回寫到內存會導致其他處理器的緩存無效,IA-32處理器和Intel 64處理器使用MESI實現緩存一致性協議。

緩存一致性是一個協議,不同處理器的具體實現會有所不同,MESI是一種比較常見的緩存一致性協議實現。

MESI協議

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

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

其中上述狀態隨著不同CPU的操作還會進行不停的變更:

一個處于M狀態的緩存行,必須時刻監聽所有試圖讀取該緩存行對應的主存地址的操作,如果監聽到,則必須在此操作執行前把其緩存行中的數據寫回CPU。

一個處于S狀態的緩存行,必須時刻監聽使該緩存行無效或者獨享該緩存行的請求,如果監聽到,則必須把其緩存行狀態設置為I。

一個處于E狀態的緩存行,必須時刻監聽其他試圖讀取該緩存行對應的主存地址的操作,如果監聽到,則必須把其緩存行狀態設置為S。

對于MESI協議,從CPU讀寫角度來說會遵循以下原則:

CPU讀數據:當CPU需要讀取數據時,如果其緩存行的狀態是I的,則需要從內存中讀取,并把自己狀態變成S,如果不是I,則可以直接讀取緩存中的值,但在此之前,必須要等待其他CPU的監聽結果,如其他CPU也有該數據的緩存且狀態是M,則需要等待其把緩存更新到內存之后,再讀取。

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

當引入總線鎖或緩存一致性協議之后,CPU、緩存、內存的結構變為下圖:

CPU-緩存-總線-內存

MESI協議帶來的問題

在上述MESI協議的交互過程中,我們已經可以看到在各個CPU之間存在大量的消息傳遞(監聽處理)。而緩存的一致性消息傳遞是需要時間的,這就使得切換時會產生延遲。一個CPU對緩存中數據的改變,可能需要獲得其他CPU的回執之后才能繼續進行,在這期間處于阻塞狀態。

Store Bufferes

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

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

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

第一步,CPU-0把a=66寫入Store Bufferes中,然后發送Invalid消息給其他CPU,無需等待其他CPU相應,便可繼續執行其他指令了。

store bufferes

第二步,當CPU-0收到其他所有CPU對Invalid通知的相應之后,再把Store Bufferes中的共享變量同步到緩存和主內存中。

store Bufferes

Store Forward(存儲轉發)

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

失效隊列

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

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

內存屏障

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

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

這種因為CPU緩存優化導致后面的指令無法感知到前面指令的執行結果,看起來就像指令之間的執行順序錯亂了一樣,對于這種現象我們俗稱“CPU亂序執行”。

亂序執行是導致多線程下程序Bug的原因,解決方案很簡單:禁用CPU緩存優化。但大多數情況下的數據并不存在共享問題,直接禁用會導致整體性能下降,得不償失。于是就提供了針對多線程共享場景的解決機制:內存屏障機制。

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

CPU層面的內存屏障

CPU層面提供了三類內存屏障:

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

下面通過一段偽代碼來進行說明:

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

上述實例中通過內存屏障防止了指令重排,能夠得到預期的結果。

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

Java內存模型

內存屏障解決了CPU緩存優化導致的指令執行的順序性和可見性問題,但不同的硬件系統提供的“內存屏障”指令又有所不同,作為開發人員也沒必要熟悉所有的內存屏障指令。而Java將不同的內存屏障指令進行了統一封裝,開發人員只需關注程序邏輯開發和內存屏障規范即可。

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

這套規范通過限定對內存的讀寫操作從而保證指令的正確性,解決了CPU多級緩存、處理器優化、指令重排序導致的內存訪問問題,保證了并發場景下的可見性。

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

JMM抽象模型結構

JMM抽象模型中將內存分為主內存和工作內存:

  • 主內存:所有線程共享,存儲實例對象、靜態字段、數組對象等存儲在堆中的變量。
  • 工作內存:每個線程獨享,線程對變量的所有操作都必須在工作內存中進行。

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

JMM抽象模型結構圖如下:

JMM抽象模型

JMM內存模型簡單概述就是:

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

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

編譯器指令重排

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

JVM能根據處理器特性(CPU多級緩存系統、多核處理器等)適當的對機器指令進行重排序,使機器指令能更符合CPU的執行特性,最大限度的發揮機器性能。

從源碼到最終執行示例圖:

指令重排序

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

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

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

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

JMM的內存屏障

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

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

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

為了實現volatile內存語義,JMM會分別限制這兩種類型的重排序類型。下圖是JMM針對編譯器制定的volatile重排序規則表。

JMM重排序

從圖中可以得出一個基本規則:

  • 當第二個操作是volatile寫時,不管第一個操作是什么,都不能重排序。這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之后。
  • 當第一個操作是volatile讀時,不管第二個操作是什么,都不能重排序。這個規則確保volatile讀之后的操作不會被編譯器重排序到volatile讀之前。
  • 當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。

為了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。對于編譯器來說,發現一個最優布置來最小化插入屏障的總數幾乎不可能。為此,JMM采取保守策略。下面是基于保守策略的JMM內存屏障插入策略:

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

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

volatile寫屏障

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

volatile讀內存屏障

JMM對volatile的特殊規則定義

JVM內存指令與volatile相關的操作有:

  • read(讀取):作用于主內存變量,把一個變量值從主內存傳輸到線程的工作內存中,以便隨后的load動作使用;
  • load(載入):作用于工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中;
  • use(使用):作用于工作內存的變量,把工作內存中的一個變量值傳遞給執行引擎,每當虛擬機遇到一個需要使用變量的值的字節碼指令時將會執行這個操作;
  • assign(賦值):作用于工作內存的變量,它把一個從執行引擎接收到的值賦值給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作;
  • store(存儲):作用于工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以便隨后的write的操作;
  • write(寫入):作用于主內存的變量,它把store操作從工作內存中一個變量的值傳送到主內存的變量中;

在對volatile修飾的變量進行操作時,需滿足以下規則:

  • 規則1:線程對變量執行的前一個動作是load時才能執行use,反之只有后一個動作是use時才能執行load。線程對變量的read,load,use動作關聯,必須連續一起出現。這保證了線程每次使用變量時都需要從主存拿到最新的值,保證了其他線程修改的變量本線程能看到。
  • 規則2:線程對變量執行的前一個動作是assign時才能執行store,反之只有后一個動作是store時才能執行assign。線程對變量的assign,store,write動作關聯,必須連續一起出現。這保證了線程每次修改變量后都會立即同步回主內存,保證了本線程修改的變量其他線程能看到。
  • 規則3:有線程T,變量V、變量W。假設動作A是T對V的use或assign動作,P是根據規則2、3與A關聯的read或write動作;動作B是T對W的use或assign動作,Q是根據規則2、3與B關聯的read或write動作。如果A先與B,那么P先與Q。這保證了volatile修飾的變量不會被指令重排序優化,代碼的執行順序與程序的順序相同。

volatile實例及分析

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

示例代碼如下:

  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() + "當前線程嗅探到initFlag的狀態的改變" + i); 
  26.  } 

根據上面的理論知識,先猜測一下線程先后打印出的內容是什么?先打印”線程threadA修改共享變量initFlag“,然后打印”線程threadB當前線程嗅探到initFlag的狀態的改變0“?

當真正執行程序時,會發現整個線程阻塞在while循環處,并未打印出第2條內容。此時JMM操作如下圖:

thread-without-volatile

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

當對變量initFlag添加了volatile修飾之后:

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

JMM操作如下圖:

thread-with-volatile

添加了volatile修飾之后,兩句日志都會被打印出來。這是因為添加volatile關鍵字后,就會有lock指令,使用緩存一致性協議,線程B中會一直嗅探initFlag是否被改變,線程A修改initFlag后會立即同步回主內存,同時通知線程B將緩存行狀態改為I(無效狀態),重新從主內存讀取。

volatile無法保證原子性

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

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

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

我們來分析一下在這個過程中會有的線程安全問題:

第一步,線程A和B同時獲得count的初始值,這一步沒什么問題;

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

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

  • 運算結果依賴變量的當前值,并且有不止一個線程在修改變量的值。
  • 變量需要與其他狀態變量共同參與不變約束

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

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

也就是說被修飾的變量值獨立于任何程序的狀態,包括變量的當前狀態。

volatile適用場景

狀態標志

使用一個布爾狀態標志,用于指示發生了一個重要的一次性事件,例如完成初始化或請求停機。

  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執行doWork()的過程中,線程2可能調用了shutdown,所以boolean變量必須是volatile。

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

一次性安全發布

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

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

  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步中實例化Singleton分多步執行(分配內存空間、初始化對象、將對象指向分配的內存空間),某些編譯器為了性能原因,會將第二步和第三步進行重排序(分配內存空間、將對象指向分配的內存空間、初始化對象)。這樣,某個線程可能會獲得一個未完全初始化的實例。

獨立觀察(independent observation)

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

另一種場景就是應用程序搜集統計信息。比如記錄最后一次登錄的用戶名,反復使用lastUser引用來發布值,以供其他程序使用。

  1. public class UserManager { 
  2.     public volatile String lastUser; //發布的信息 
  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 模式的基本原理是:很多框架為易變數據的持有者(例如 HttpSession)提供了容器,但是放入這些容器中的對象必須是線程安全的。在 volatile bean 模式中,JavaBean 的所有數據成員都是 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.     } 

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

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

如下線程安全的計數器代碼,使用 synchronized 確保增量操作是原子的,并使用 volatile 保證當前結果的可見性。如果更新不頻繁的話,該方法可實現更好的性能,因為讀路徑的開銷僅僅涉及 volatile 讀操作,這通常要優于一個無競爭的鎖獲取的開銷。

  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。因為x++不是原子操作 
  13.     public synchronized int increment() { 
  14.         return value++; 
  15.     } 

使用鎖進行有變化的操作,使用volatile進行只讀操作。volatile允許多個線程同時執行讀操作。

小結

本文先從硬件層面分析CPU的處理機制,為了優化CPU引入了緩存,為了更進一步優化引入了Store Bufferes,而Store Bufferes導致了緩存一致性問題。于是有了總線鎖和緩存一致性協議(EMSI實現),從而有了CPU的內存屏障機制。

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

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

想必通過這篇文章,你已經徹底弄懂了volatile相關的知識了吧?來,關注一波。

 

責任編輯:武曉燕 來源: 程序新視界
相關推薦

2009-12-07 17:20:29

PHP stdClas

2009-02-20 10:59:21

Vista幫助系統使用技巧

2010-04-27 17:06:16

AIX vmstat

2010-04-21 14:56:23

Unix 線程

2009-10-23 15:30:53

無線接入技術

2019-10-21 15:30:54

JS技巧前端

2011-07-25 16:25:47

2011-07-08 13:56:00

域控制器服務器

2009-07-01 17:58:20

JSP

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++動態內存開發

2012-01-10 10:05:47

文件目錄訪問控制UGO

2009-12-01 11:33:03

PHP判斷字符串的包含

2012-02-04 14:56:52

JP1數據中心

2010-05-27 13:32:36

IIS服務安全認證

2011-08-23 18:30:59

MySQLTIMESTAMP

2016-10-08 12:46:08

Linux監控限制

2021-08-26 06:58:14

CookieSession應用
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 亚洲高清av在线 | 97精品国产 | 99精品免费| 欧美国产精品一区二区三区 | 青青草网站在线观看 | 91污在线| 国产激情网| 国产精品一区二区不卡 | 亚洲成人av | 视频一区在线 | 亚洲男人天堂2024 | 欧美精品一区二区三区在线四季 | 国产粉嫩尤物极品99综合精品 | 久久久激情视频 | 中文字幕在线免费观看 | 亚洲国产情侣 | 99久久精品免费 | 亚洲一区 中文字幕 | 久久99精品久久久久 | 91精品国产欧美一区二区 | 亚洲综合色视频在线观看 | 午夜小视频在线观看 | 国产在线看片 | 午夜免费看视频 | 成人在线免费视频 | 午夜小视频在线播放 | 成人乱人乱一区二区三区软件 | 日本久草视频 | 日本黄色一级片视频 | 永久免费av | 一色桃子av一区二区 | 国产情侣在线看 | av在线播放网 | 日本在线免费观看 | 亚洲三级在线观看 | a级片在线观看 | 亚洲精品久久久久久下一站 | 日韩快播电影网 | 99福利在线观看 | 国产精品视频999 | 精品一区二区免费视频 |