【阿里面試系列】併發編程之Volatile的做用及原理

文章簡介

分析volatile的做用以及底層實現原理,這也是大公司喜歡問的問題java

內容導航

  1. volatile的做用
  2. 什麼是可見性
  3. volatile源碼分析

volatile的做用

在多線程中,volatile和synchronized都起到很是重要的做用,synchronized是經過加鎖來實現線程的安全性。而volatile的主要做用是在多處理器開發中保證共享變量對於多線程的可見性。
可見性的意思是,當一個線程修改一個共享變量時,另一個線程能讀取到修改之後的值。接下來經過一個簡單的案例來演示可見性問題linux

public class VolatileDemo {
    private /*volatile*/ static boolean stop=false; //添加volatile修飾和不添加volatile修飾的演示效果
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            int i=0;
            while(!stop){
                i++;
            }
        });
        thread.start();
        System.out.println("begin start thread");
        Thread.sleep(1000);
        stop=true;
    }
}
  1. 定義一個共享變量 stop
  2. 在main線程中建立一個子線程 thread,子線程讀取到 stop的值作循環結束的條件
  3. main線程中修改stop的值爲 true
  4. 當 stop沒有增長volatile修飾時,子線程對於主線程的 stop=true的修改是不可見的,這樣將致使子線程出現死循環
  5. 當 stop增長了volatile修飾時,子線程能夠獲取到主線程對於 stop=true的值,子線程while循環條件不知足退出循環
    增長volatile關鍵字之後,main線程對於共享變量 stop值的更新,對於子線程 thread可見,這就是volatile的做用

這段代碼有些人測試不出效果,是由於JVM沒有優化致使的,在cmd控制檯輸入java -version,若是顯示的是 JavaHotSpot(TM)ServerVM,就能正常演示,若是是 JavaHotSpot(TM)ClientVM,須要設置成 Server模式c++

什麼是可見性,以及volatile是如何保證可見性的呢?數據庫

什麼是可見性

在併發編程中,線程安全問題的本質其實就是 原子性、有序性、可見性;接下來主要圍繞這三個問題進行展開分析其本質,完全瞭解可見性的特性編程

  1. 原子性 和數據庫事務中的原子性同樣,知足原子性特性的操做是不可中斷的,要麼所有執行成功要麼所有執行失敗
  2. 有序性 編譯器和處理器爲了優化程序性能而對指令序列進行重排序,也就是你編寫的代碼順序和最終執行的指令順序是不一致的,重排序可能會致使多線程程序出現內存可見性問題2018已通過去過去,2019還想一成不變嗎?擁抱變化,突破瓶頸,想要學習Java架構技術的朋友能夠加個人羣:725219329,羣內每晚都會有阿里技術大牛講解的最新Java架構技術。並會錄製錄播視頻分享在羣公告中,做爲給廣大朋友的加羣的福利——分佈式(Dubbo、Redis、RabbitMQ、Netty、RPC、Zookeeper、高併發、高可用架構)/微服務(Spring Boot、Spring Cloud)/源碼(Spring、Mybatis)/性能優化(JVM、TomCat、MySQL)
  3. 可見性 多個線程訪問同一個共享變量時,其中一個線程對這個共享變量值的修改,其餘線程可以馬上得到修改之後的值
    爲了完全瞭解這三個特性,咱們從兩個層面來分析,第一個層面是硬件層面、第二個層面是JMM層面

從硬件層面分析三大特性

原子性、有序性、可見性這些問題,咱們能夠認爲是基於多核心CPU架構下的存在的問題。由於在單核CPU架構下,全部的線程執行都是基於CPU時間片切換,因此不存在併發問題 (在IntelPentium4開始,引入了超線程技術,也就是一個CPU核心模擬出2個線程的CPU,實現多線程並行)。數組

CPU高速緩存

線程設計的目的是充分利用CPU達到實時性的效果,可是不少時候CPU的計算任務還須要和內存進行交互,好比讀取內存中的運算數據、將處理結果寫入到內存。在理想狀況下,存儲器應該是很是快速的執行一條指令,這樣CPU就不會受到存儲器的限制。但目前技術沒法知足,因此就出現了其餘的處理方式。緩存

 

 

存儲器頂層是CPU中的寄存器,存儲容量小,可是速度和CPU同樣快,因此CPU在訪問寄存器時幾乎沒有延遲;接下來就是CPU的高速緩存;最後就是內存。安全

 

高速緩存從下到上越接近CPU訪問速度越快,同時容量也越小。如今的大部分處理器都有二級或者三級緩存,分別是L1/L2/L3, L1又分爲L1-d的數據緩存和L1-i的指令緩存。其中L3緩存是在多核CPU之間共享的。性能優化

原子性

在多核CPU架構下,在同一時刻對同一共享變量執行 decl指令(遞減指令,至關於i--,它分爲三個過程:讀->改->寫,這個指令涉及到兩次內存操做,那麼在這種狀況下i的結果是沒法預測的。這就是原子性問題多線程

處理器如何解決原子性問題呢?

其實這個問題稍微提煉一下,無非就是多線程並行訪問同一個共享資源的時候的原子性問題,若是把問題放大到分佈式架構裏面,這個問題的解決方法就是鎖。因此在CPU層面,提供了兩種鎖的機制來保證原子性

總線鎖

若是多個處理器同時對同一共享變量進行 decl指令操做,那這個操做必定不是原子的,也就是執行的結果和預期結果不一致。以下圖所示,咱們指望的結果是3,可是有可能結果是2

 

若是要解決這個問題,就須要是的CPU0在更新共享變量時,CPU1就不能操做緩存了該共享變量內存地址的緩存,因此處理器提供了總線鎖來解決問題,處理器會提供一個LOCK#信號,當一個處理器在總線上輸出這個信號時,其餘處理器的請求會被阻塞,那麼該處理器就能夠獨佔共享內存

總線鎖有一個弊端,總線鎖至關於使得多個CPU由並行執行變成了串行,使得CPU的性能嚴重降低,因此在P6系列之後的處理器中,引入了緩存鎖。

緩存鎖

咱們只須要保證 多個線程操做同一個被緩存的共享數據的原子性就行,因此只須要鎖定被緩存的共享對象便可。所謂緩存鎖是指被緩存在處理器中的共享數據,在Lock操做期間被鎖定,那麼當被修改的共享內存的數據回寫到內存時,處理器不在總線上聲明LOCK#信號,而是修改內部的內存地址,並經過 緩存一致性機制來保證操做的原子性。2018已通過去過去,2019還想一成不變嗎?擁抱變化,突破瓶頸,想要學習Java架構技術的朋友能夠加個人羣:725219329,羣內每晚都會有阿里技術大牛講解的最新Java架構技術。並會錄製錄播視頻分享在羣公告中,做爲給廣大朋友的加羣的福利——分佈式(Dubbo、Redis、RabbitMQ、Netty、RPC、Zookeeper、高併發、高可用架構)/微服務(Spring Boot、Spring Cloud)/源碼(Spring、Mybatis)/性能優化(JVM、TomCat、MySQL)

什麼是緩存一致性呢?
所謂緩存一致性,就是多個CPU核心中緩存的同一共享數據的數據一致性,而(MESI)使用比較普遍的緩存一致性協議。MESI協議其實是表示緩存的四種狀態
M(Modify) 表示共享數據只緩存在當前CPU緩存中,而且是被修改狀態,也就是緩存的數據和主內存中的數據不一致
E(Exclusive) 表示緩存的獨佔狀態,數據只緩存在當前CPU緩存中,而且沒有被修改
S(Shared) 表示數據可能被多個CPU緩存,而且各個緩存中的數據和主內存數據一致
I(Invalid) 表示緩存已經失效

每一個CPU核心不只僅知道本身的讀寫操做,也會監聽其餘Cache的讀寫操做
CPU的讀取會遵循幾個原則

 

 

 

  1. 若是緩存的狀態是I,那麼就從內存中讀取,不然直接從緩存讀取
  2. 若是緩存處於M或者E的CPU 嗅探到其餘CPU有讀的操做,就把本身的緩存寫入到內存,並把本身的狀態設置爲S
  3. 只有緩存狀態是M或E的時候,CPU才能夠修改緩存中的數據,修改後,緩存狀態變爲M

可見性

CPU高速緩存以及指令重排序都會形成可見性問題,接下來從兩個角度來分析

MESI優化帶來的可見性問題

前面說過MESI協議,也就是緩存一致性協議。這個協議存在一個問題,就是當CPU0修改當前緩存的共享數據時,須要發送一個消息給其餘緩存了相同數據的CPU核心,這個消息傳遞給其餘CPU核心以及收到消息完成各自緩存狀態的切換這個過程當中,CPU會等待全部緩存響應完成,這樣會下降處理器的性能。爲了解決這個問題,引入了 StoreBufferes存儲緩存。

處理器把須要寫入到主內存中的值先寫入到存儲緩存中,而後繼續去處理其餘指令。當全部的CPU核心返回了失效確認時,數據纔會被最終提交。可是這種優化又會帶來另外的問題。
若是某個CPU嘗試將其餘CPU佔有的共享數據寫入到內存,消息提交給store buffer之後,當前CPU繼續作其餘事情,而若是後面的指令依賴於這個被寫入內存的最新數據(因爲store buffer尚未寫入到內存),就會產生可見性問題(也就是值尚未更新到內存中,這個時候讀取到的共享數據的值是錯誤的)。

Store Bufferes帶來的CPU內存的亂序訪問致使的可見性問題

Store Bufferes中的數據什麼時候寫入到內存中是不肯定的,那麼意味着這個過程的執行順序也是不肯定的,好比下面這個例子
exeToCPU0和exeToCPU1分別在兩個獨立的cpu核心上執行,假如CPU0 緩存了 isFinish這個共享變量,而且狀態爲(E->獨佔),而value多是(S共享狀態被其餘CPU核心修改之後變爲I(失效狀態)。
這種狀況下value的緩存數據變動路徑爲, value將失效狀態須要響應給觸發緩存更新的CPU核心,接着該CPU將 StoreBufferes寫入到內存,這就會致使value會比isFinish更遲的拋棄存儲緩存。那麼就可能出現CPU1讀取到了isFinish的值爲true,而value的值不等於10的狀況。
這種CPU的內存亂序訪問,會帶來可見性問題。

value = 3;
void exeToCPU0(){
  value = 10;
  isFinsh = true;
}
void exeToCPU1(){
  if(isFinsh){
    assert value == 10;
  }
}

CPU層面的內存屏障

什麼是內存屏障?從前面的內容基本能有一個初步的猜測,內存屏障就是將 store bufferes中的指令寫入到內存,從而使得其餘訪問同一共享內存的線程的可見性。
X86的memory barrier指令包括lfence(讀屏障) sfence(寫屏障) mfence(全屏障)

  • Store Memory Barrier(寫屏障) 告訴處理器在寫屏障以前的全部已經存儲在存儲緩存(store bufferes)中的數據同步到主內存,簡單來講就是使得寫屏障以前的指令的結果對屏障以後的讀或者寫是可見的
  • Load Memory Barrier(讀屏障) 處理器在讀屏障以後的讀操做,都在讀屏障以後執行。配合寫屏障,使得寫屏障以前的內存更新對於讀屏障以後的讀操做是可見的
  • Full Memory Barrier(全屏障) 確保屏障前的內存讀寫操做的結果提交到內存以後,再執行屏障後的讀寫操做

有了內存屏障之後,對於上面這個例子,咱們能夠這麼來改,從而避免出現可見性問題

value = 3;
void exeToCPU0(){
  value = 10;
  storeMemoryBarrier(); //這個是一個僞代碼,插入一個寫屏障,使得value=10這個值強制寫入到主內存中
  isFinsh = true;
}
void exeToCPU1(){
  if(isFinsh){
    loadMemoryBarrier();//僞代碼,插入一個讀屏障,使得cpu1從主內存中得到最新的數據
    assert value == 10;
  }
}

總的來講,內存屏障的做用能夠經過防止CPU對內存的亂序訪問來保證共享數據在多線程並行執行下的可見性

有序性

有序性簡單來講就是程序代碼執行的順序是否按照咱們編寫代碼的順序執行,通常來講,爲了提升性能,編譯器和處理器會對指令作重排序,重排序分3類

  1. 編譯器優化重排序,在不改變單線程程序語義的前提下,改變代碼的執行順序
  2. 指令集並行的重排序,對於不存在數據依賴的指令,處理器能夠改變語句對應指令的執行順序來充分利用CPU資源
  3. 內存系統的重排序,也就是前面說的CPU的內存亂序訪問問題3.

也就是說,咱們編寫的源代碼到最終執行的指令,會通過三種重排序

 

有序性會帶來可見性問題,因此能夠經過內存屏障指令來進制特定類型的處理器重排序

從JMM層面解決線程併發問題

從硬件層面的分析瞭解到原子性、有序性、可見性的本質之後,知道硬件層面針對這三個問題的解決辦法,原子性是經過總線鎖或緩存鎖來實現,而有序性和可見性能夠經過內存屏障來解決。那麼在軟件層面,如何解決原子性、有序性、可見性問題呢?答案就是: JMM(JavaMemoryModel)內存模型

硬件層面的原子性、有序性、可見性在不一樣的CPU架構和操做系統中的實現可能都不同,而Java語言的特性是 write once,run anywhere,意味着JVM層面須要屏蔽底層的差別,所以在JVM規範中定義了JMM。

 

(JMM內存模型的抽象結構)

JMM屬於語言級別的抽象內存模型,能夠簡單理解爲對硬件模型的抽象,它定義了共享內存中多線程程序讀寫操做的行爲規範,也就是在虛擬機中將共享變量存儲到內存以及從內存中取出共享變量的底層細節。
經過這些規則來規範對內存的讀寫操做從而保證指令的正確性,它解決了CPU多級緩存、處理器優化、指令重排序致使的內存訪問問題,保證了併發場景下的可見性。
須要注意的是,JMM並無限制執行引擎使用處理器的寄存器或者高速緩存來提高指令執行速度,也沒有限制編譯器對指令進行重排序,也就是說在JMM中,也會存在緩存一致性問題和指令重排序問題。只是JMM把底層的問題抽象到JVM層面,再基於CPU層面提供的內存屏障指令,以及限制編譯器的重排序來解決併發問題

Java內存模型定義了線程和內存的交互方式,在JMM抽象模型中,分爲主內存、工做內存;主內存是全部線程共享的,通常是實例對象、靜態字段、數組對象等存儲在堆內存中的變量。工做內存是每一個線程獨佔的,線程對變量的全部操做都必須在工做內存中進行,不能直接讀寫主內存中的變量,線程之間的共享變量值的傳遞都是基於主內存來完成。
在JMM中,定義了8個原子操做來實現一個共享變量如何從主內存拷貝到工做內存,以及如何從工做內存同步到主內存,交互以下

8個原子操做指令
lock(鎖定):做用於主內存的變量,把一個變量標識爲一條線程獨佔狀態。
unlock(解鎖):做用於主內存變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定。
read(讀取):做用於主內存變量,把一個變量值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用
load(載入):做用於工做內存的變量,它把read操做從主內存中獲得的變量值放入工做內存的變量副本中。
use(使用):做用於工做內存的變量,把工做內存中的一個變量值傳遞給執行引擎,每當虛擬機遇到一個須要使用變量的值的字節碼指令時將會執行這個操做。
assign(賦值):做用於工做內存的變量,它把一個從執行引擎接收到的值賦值給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做。
store(存儲):做用於工做內存的變量,把工做內存中的一個變量的值傳送到主內存中,以便隨後的write的操做。
write(寫入):做用於主內存的變量,它把store操做從工做內存中一個變量的值傳送到主內存的變量中。

順序一致性

若是要把一個變量從主內存中複製到工做內存,就須要按順尋地執行read和load操做,若是把變量從工做內存中同步回主內存中,就要按順序地執行store和write操做。JMM只要求這兩個操做必須按順序執行,而沒有保證必須是連續執行。也就是read和load之間,store和write之間是能夠插入其餘指令的,如對主內存中的變量a、b進行訪問時,可能的順序是read a,read b,load b, load a。

JMM不保證未同步程序的執行結果與該程序在順序一致性模型中的執行結果一致,由於若是想要保證執行結果一致,意味着JMM須要進制處理器和編譯器的優化,這對於程序的執行性能會產生很大的影響。因此在未同步程序的執行中,因爲執行順序的不肯定性致使結果沒法預測。咱們可使用同步原語好比 synchronized,volatile、final來實現程序的同步操做來保證順序一致性

假若有兩個線程A和B並行執行,A和B線程分別都有3個操做,在程序中的順序是 A1->A2->A3, B1->B2->B3。
假設這兩個程序沒有使用同步原語,那麼線程並行執行的效果多是

 

若是這兩個程序使用了監視器鎖來實現正確同步,那麼執行的過程必定是

 

此圖來自併發編程的藝術

重排序

CPU層面的內存亂序訪問屬於重排序的一部分,同時咱們還提到了編譯器的優化執行的重排序。重排序是一種優化手段,可是在多線程併發中,會致使可見性問題。
編譯器的重排序是指,在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序來優化程序的性能.2018已通過去過去,2019還想一成不變嗎?擁抱變化,突破瓶頸,想要學習Java架構技術的朋友能夠加個人羣:725219329,羣內每晚都會有阿里技術大牛講解的最新Java架構技術。並會錄製錄播視頻分享在羣公告中,做爲給廣大朋友的加羣的福利——分佈式(Dubbo、Redis、RabbitMQ、Netty、RPC、Zookeeper、高併發、高可用架構)/微服務(Spring Boot、Spring Cloud)/源碼(Spring、Mybatis)/性能優化(JVM、TomCat、MySQL)
編譯器的重排序和CPU的重排序的原則同樣,會遵照數據依賴性原則,編譯器和處理器不會改變存在數據依賴關係的兩個操做的執行順序,好比下面的代碼,這三種狀況在單線程裏面若是改變代碼的執行順序,都會致使結果不一致,因此重排序不會對這類的指令作優化,也就是須要知足 as-if-serial語義

//寫後讀
a=1;
b=1;
//寫後寫
a=1;
a=2;
//讀後寫
a=b;
b=1;

as-if-serial語義
as-if-serial語義的意思是無論怎麼重排序,單線程程序的執行結果不能被改變,編譯器、處理器都必須遵照這個語義

JMM層面的內存屏障

爲了保證內存可見性,Java編譯器在生成指令序列的適當位置會插入內存屏障來禁止特定類型的處理器的重排序,在JMM中把內存屏障分爲四類

 

屏障的做用這裏就不重複再說了,實際上JMM層面的內存屏障就是對CPU層面的內存屏障指令作的包裝,做用是經過在合適的位置插入內存屏障來保證可見性

JVM是如何在JMM層面解決原子性、有序性、可見性問題的呢?

相信經過上面的分析,基本上有了答案

  1. 原子性:Java中提供了兩個高級指令 monitorenter和 monitorexit,也就是對應的synchronized同步鎖來保證原子性
  2. 可見性:volatile、synchronized、final均可以解決可見性問題
  3. 有序性:synchronized和volatile能夠保證多線程之間操做的有序性,volatile會禁止指令重排序

volatile源碼分析

若是你看到這個章節了,意味着你對可見性有一個清晰的認識了,也知道JMM是基於禁止指令重排序來實現可見性的,那麼咱們再來分析volatile的源碼,就會簡單不少

基於最開始演示的這段代碼做爲入口

public class VolatileDemo {
    public volatile static boolean stop=false;
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            int i=0;
            while(!stop){
                i++;
            }
        });
        thread.start();
        System.out.println("begin start thread");
        Thread.sleep(1000);
        stop=true;
    }
}

經過 javap-vVolatileDemo.class查看字節碼指令

public static volatile boolean stop;
    descriptor: Z
    flags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE
...//省略
 public static void main(java.lang.String[]) throws java.lang.InterruptedException;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: new           #2                  // class java/lang/Thread
         3: dup
         4: invokedynamic #3,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;
         9: invokespecial #4                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
        12: astore_1
        13: aload_1
        14: invokevirtual #5                  // Method java/lang/Thread.start:()V
        17: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        20: ldc           #7                  // String begin start thread
        22: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        25: ldc2_w        #9                  // long 1000l
        28: invokestatic  #11                 // Method java/lang/Thread.sleep:(J)V
        31: iconst_1
        32: putstatic     #12                 // Field stop:Z
        35: return

注意被修飾了volatile關鍵字的 stop字段,會多一個 ACC_VOLATILE的flag,在給 stop複製的時候,調用的字節碼是 putstatic,這個字節碼會經過BytecodeInterpreter解釋器來執行,找到Hotspot的源碼 bytecodeInterpreter.cpp文件,搜索 putstatic指令定位到代碼

CASE(_putstatic):
        {
          u2 index = Bytes::get_native_u2(pc+1);
          ConstantPoolCacheEntry* cache = cp->entry_at(index);
          if (!cache->is_resolved((Bytecodes::Code)opcode)) {
            CALL_VM(InterpreterRuntime::resolve_get_put(THREAD, (Bytecodes::Code)opcode),
                    handle_exception);
            cache = cp->entry_at(index);
          }

#ifdef VM_JVMTI
          if (_jvmti_interp_events) {
            int *count_addr;
            oop obj;
            // Check to see if a field modification watch has been set
            // before we take the time to call into the VM.
            count_addr = (int *)JvmtiExport::get_field_modification_count_addr();
            if ( *count_addr > 0 ) {
              if ((Bytecodes::Code)opcode == Bytecodes::_putstatic) {
                obj = (oop)NULL;
              }
              else {
                if (cache->is_long() || cache->is_double()) {
                  obj = (oop) STACK_OBJECT(-3);
                } else {
                  obj = (oop) STACK_OBJECT(-2);
                }
                VERIFY_OOP(obj);
              }

              CALL_VM(InterpreterRuntime::post_field_modification(THREAD,
                                          obj,
                                          cache,
                                          (jvalue *)STACK_SLOT(-1)),
                                          handle_exception);
            }
          }
#endif /* VM_JVMTI */

          // QQQ Need to make this as inlined as possible. Probably need to split all the bytecode cases
          // out so c++ compiler has a chance for constant prop to fold everything possible away.

          oop obj;
          int count;
          TosState tos_type = cache->flag_state();

          count = -1;
          if (tos_type == ltos || tos_type == dtos) {
            --count;
          }
          if ((Bytecodes::Code)opcode == Bytecodes::_putstatic) {
            Klass* k = cache->f1_as_klass();
            obj = k->java_mirror();
          } else {
            --count;
            obj = (oop) STACK_OBJECT(count);
            CHECK_NULL(obj);
          }

          //
          // Now store the result
          //
          int field_offset = cache->f2_as_index();
          if (cache->is_volatile()) {
            if (tos_type == itos) {
              obj->release_int_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == atos) {
              VERIFY_OOP(STACK_OBJECT(-1));
              obj->release_obj_field_put(field_offset, STACK_OBJECT(-1));
              OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
            } else if (tos_type == btos) {
              obj->release_byte_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ltos) {
              obj->release_long_field_put(field_offset, STACK_LONG(-1));
            } else if (tos_type == ctos) {
              obj->release_char_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == stos) {
              obj->release_short_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ftos) {
              obj->release_float_field_put(field_offset, STACK_FLOAT(-1));
            } else {
              obj->release_double_field_put(field_offset, STACK_DOUBLE(-1));
            }
            OrderAccess::storeload();
          } else {
            if (tos_type == itos) {
              obj->int_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == atos) {
              VERIFY_OOP(STACK_OBJECT(-1));
              obj->obj_field_put(field_offset, STACK_OBJECT(-1));
              OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
            } else if (tos_type == btos) {
              obj->byte_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ltos) {
              obj->long_field_put(field_offset, STACK_LONG(-1));
            } else if (tos_type == ctos) {
              obj->char_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == stos) {
              obj->short_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ftos) {
              obj->float_field_put(field_offset, STACK_FLOAT(-1));
            } else {
              obj->double_field_put(field_offset, STACK_DOUBLE(-1));
            }
          }
...//省略不少代碼

其餘代碼不用管,直接看 cache->is_volatile()這段代碼,cache是 stop在常量池緩存中的一個實例,這段代碼是判斷這個cache是不是被 volatile修飾, is_volatile()方法的定義在 accessFlags.hpp文件中,代碼以下

public:
  // Java access flags
  ...//
  bool is_volatile    () const         { return (_flags & JVM_ACC_VOLATILE    ) != 0; }
  bool is_transient   () const         { return (_flags & JVM_ACC_TRANSIENT   ) != 0; }
  bool is_native      () const         { return (_flags & JVM_ACC_NATIVE      ) != 0; }

is_volatile是判斷是否有 ACC_VOLATILE這個flag,很顯然,經過 volatile修飾的stop的字節碼中是存在這個flag的,因此 is_volatile()返回true
接着,根據當前字段的類型來給 stop賦值,執行 release_byte_field_put方法賦值,這個方法的實如今 oop.inline.hpp中

inline void oopDesc::release_byte_field_put(int offset, jbyte contents)     
{ OrderAccess::release_store(byte_field_addr(offset), contents); }

賦值的動做被包裝了一層,看看 OrderAccess::release_store作了什麼事情呢?這個方法的定義在 orderAccess.hpp中,具體的實現,根據不一樣的操做系統和CPU架構,調用不一樣的實現

 

以 orderAccess_linux_x86.inline.hpp爲例,找到 OrderAccess::release_store的實現,代碼以下

inline void     OrderAccess::release_store(volatile jbyte*   p, jbyte   v) { *p = v; }

能夠看到其實Java的volatile操做,在JVM實現層面第一步是給予了C++的原語實現。c/c++中的volatile關鍵字,用來修飾變量,一般用於語言級別的 memory barrier。被volatile聲明的變量表示隨時可能發生變化,每次使用時,都必須從變量i對應的內存地址讀取,編譯器對操做該變量的代碼再也不進行優化

賦值操做完成之後,若是你們仔細看了前面putstatic的代碼,就會發現還會執行一個 OrderAccess::storeload();的代碼,這個代碼的實現是在 orderAccess_linux_x86.inline.hpp,它其實就是一個storeload內存屏障,JVM層面的四種內存屏障的定義以及實現

inline void OrderAccess::loadload()   { acquire(); }
inline void OrderAccess::storestore() { release(); }
inline void OrderAccess::loadstore()  { acquire(); }
inline void OrderAccess::storeload()  { fence(); }

當調用 storeload屏障時,它會調用fence()方法

inline void OrderAccess::fence() {
  if (os::is_MP()) { //返回是否多處理器,若是是多處理器纔有必要增長內存屏障
    // always use locked addl since mfence is sometimes expensive
#ifdef AMD64
    //__asm__ volatile 嵌入彙編指令
    //lock 彙編指令,lock指令會鎖住操做的緩存行,也就是緩存鎖的實現
    __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  }
}

os::is_MP()判斷是不是多核,若是是單核,那麼就不存在內存不可見或者亂序的問題 volatile:禁止編譯器對代碼進行某些優化.
Lock :彙編指令,lock指令會鎖住操做的緩存行(cacheline), 通常用於read-Modify-write的操做;用來保證後續的操做是原子的
cc表明的是寄存器,memory表明是內存;這邊同時用了」cc」和」memory」,來通知編譯器內存或者寄存器內的內容已經發生了修改,要從新生成加載指令(不能夠從緩存寄存器中取)
這邊的read/write請求不能越過lock指令進行重排,那麼全部帶有lock prefix指令(lock ,xchgl等)都會構成一個自然的x86 Mfence(讀寫屏障),這裏用lock指令做爲內存屏障,而後利用asm volatile("" ::: "cc,memory")做爲編譯器屏障. 這裏並無使用x86的內存屏障指令(mfence,lfence,sfence),應該是跟x86的架構有關係,x86處理器是強一致內存模型2018已通過去過去,2019還想一成不變嗎?擁抱變化,突破瓶頸,想要學習Java架構技術的朋友能夠加個人羣:725219329,羣內每晚都會有阿里技術大牛講解的最新Java架構技術。並會錄製錄播視頻分享在羣公告中,做爲給廣大朋友的加羣的福利——分佈式(Dubbo、Redis、RabbitMQ、Netty、RPC、Zookeeper、高併發、高可用架構)/微服務(Spring Boot、Spring Cloud)/源碼(Spring、Mybatis)/性能優化(JVM、TomCat、MySQL)

storeload屏障是固定調用的方法?爲何要固定調用呢?

緣由是:避免volatile寫與後面可能有的volatile讀/寫操做重排序。由於編譯器經常沒法準確判斷在一個volatile寫的後面是否須要插入一個StoreLoad屏障。爲了保證能正確實現volatile的內存語義,JMM在採起了保守策略:在每一個volatile寫的後面,或者在每一個volatile讀的前面插入一個StoreLoad屏障。由於volatile寫-讀內存語義的常見使用模式是:一個寫線程寫volatile變量,多個讀線程讀同一個volatile變量。當讀線程的數量大大超過寫線程時,選擇在volatile寫以後插入StoreLoad屏障將帶來可觀的執行效率的提高。從這裏能夠看到JMM在實現上的一個特色:首先確保正確性,而後再去追求執行效率

總結

綜上分析能夠得知,volatile是經過防止指令重排序來實現多線程對於共享內存的可見性。

相關文章
相關標籤/搜索