Volatile的作用及原理

內容導航

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

volatile的作用

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

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模式

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

什麼是可見性

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

  1. 原子性 和數據庫事務中的原子性一樣,滿足原子性特性的操作是不可中斷的,要麼全部執行成功要麼全部執行失敗
  2. 有序性 編譯器和處理器爲了優化程序性能而對指令序列進行重排序,也就是你編寫的代碼順序和最終執行的指令順序是不一致的,重排序可能會導致多線程程序出現內存可見性問題
  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#信號,而是修改內部的內存地址,並通過 緩存一致性機制來保證操作的原子性。

什麼是緩存一致性呢?
所謂緩存一致性,就是多個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層面的內存亂序訪問屬於重排序的一部分,同時我們還提到了編譯器的優化執行的重排序。重排序是一種優化手段,但是在多線程併發中,會導致可見性問題。
編譯器的重排序是指,在不改變單線程程序語義的前提下,可以重新安排語句的執行順序來優化程序的性能.
編譯器的重排序和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處理器是強一致內存模型

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

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

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章