幹掉面試官2-volatile底層原理詳解


volatile關鍵字是Java虛擬機提供的最輕量級的同步機制。在多線程編程中volatilesynchronized都起着舉足輕重的作用,沒有這兩者,也就沒有那麼多JUC供我們使用。

本文會介紹volatile的作用,着重講解volatile的底層實現原理。由於volatile的出現和CPU緩存有關,也會介紹CPU緩存的相關內容,讓我們更清晰的理解volatile原理的來龍去脈。

一、volatile的作用

併發編程中有3大重要特性,瞭解一下:

  • 原子性

    一個操作或者多個操作,要麼全部執行成功,要麼全部執行失敗。滿足原子性的操作,中途不可被中斷。

  • 可見性

    多個線程共同訪問共享變量時,某個線程修改了此變量,其他線程能立即看到修改後的值。

  • 有序性

    程序執行的順序按照代碼的先後順序執行。(由於JMM模型中允許編譯器和處理器爲了效率,進行指令重排序的優化。指令重排序在單線程內表現爲串行語義,在多線程中會表現爲無序。那麼多線程併發編程中,就要考慮如何在多線程環境下可以允許部分指令重排,又要保證有序性)

synchronized關鍵字同時保證上述三種特性。

  • synchronized是同步鎖,同步塊內的代碼相當於同一時刻單線程執行,故不存在原子性和指令重排序的問題
  • synchronized關鍵字的語義JMM有兩個規定,保證其實現內存可見性:
    • 線程解鎖前,必須把共享變量的最新值刷新到主內存中;
    • 線程加鎖前,將清空工作內存中共享變量的值,從主內存中沖洗取值。

volatile關鍵字作用的是保證可見性有序性,並不保證原子性。

那麼,volatile是如何保證可見性有序性的?我們先進行基於JMM層面的實現基礎,後面兩章會進行底層原理的介紹。

1.1、volatile變量的可見性

Java虛擬機規範中定義了一種Java內存 模型(Java Memory Model,即JMM)來屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到一致的併發效果。Java內存模型的主要目標就是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的細節

JMM中規定所有的變量都存儲在主內存(Main Memory)中,每條線程都有自己的工作內存(Work Memory),線程的工作內存中保存了該線程所使用的變量的從主內存中拷貝的副本。線程對於變量的讀、寫都必須在工作內存中進行,而不能直接讀、寫主內存中的變量。同時,本線程的工作內存的變量也無法被其他線程直接訪問,必須通過主內存完成。

整體內存模型如下圖所示:

JMM內存模型

對於普通共享變量,線程A將變量修改後,體現在此線程的工作內存。在尚未同步到主內存時,若線程B使用此變量,從主內存中獲取到的是修改前的值,便發生了共享變量值的不一致,也就是出現了線程的可見性問題

volatile定義:

  • 當對volatile變量執行寫操作後,JMM會把工作內存中的最新變量值強制刷新到主內存
  • 寫操作會導致其他線程中的緩存無效

這樣,其他線程使用緩存時,發現本地工作內存中此變量無效,便從主內存中獲取,這樣獲取到的變量便是最新的值,實現了線程的可見性。

1.2、volatile變量的禁止指令重排序

volatile是通過編譯器在生成字節碼時,在指令序列中添加“內存屏障”來禁止指令重排序的。

硬件層面的“內存屏障”:

  • sfence:即寫屏障(Store Barrier),在寫指令之後插入寫屏障,能讓寫入緩存的最新數據寫回到主內存,以保證寫入的數據立刻對其他線程可見
  • lfence:即讀屏障(Load Barrier),在讀指令前插入讀屏障,可以讓高速緩存中的數據失效,重新從主內存加載數據,以保證讀取的是最新的數據。
  • mfence:即全能屏障(modify/mix Barrier ),兼具sfence和lfence的功能
  • lock 前綴:lock不是內存屏障,而是一種鎖。執行時會鎖住內存子系統來確保執行順序,甚至跨多個CPU。

JMM層面的“內存屏障”:

  • LoadLoad屏障: 對於這樣的語句Load1; LoadLoad; Load2,在Load2及後續讀取操作要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。

  • StoreStore屏障:對於這樣的語句Store1; StoreStore; Store2,在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。

  • LoadStore屏障:對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操作被刷出前,保證Load1要讀取的數據被讀取完畢。

  • StoreLoad屏障: 對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。

JVM的實現會在volatile讀寫前後均加上內存屏障,在一定程度上保證有序性。如下所示:

LoadLoadBarrier
volatile 讀操作
LoadStoreBarrier

StoreStoreBarrier
volatile 寫操作
StoreLoadBarrier

二、volatile的的底層實現

這一章會從Java代碼、字節碼、Jdk源碼、彙編層面、硬件層面去揭開volatile的面紗。

2.1、 Java代碼層面

上一段最簡單的代碼,volatile用來修飾Java變量

public class TestVolatile {

    public static volatile int counter = 1;

    public static void main(String[] args){
        counter = 2;
        System.out.println(counter);
    }
}

2.2、字節碼層面

通過javac TestVolatile.java將類編譯爲class文件,再通過javap -v TestVolatile.class命令反編譯查看字節碼文件。

打印內容過長,截圖其中的一部分:

在這裏插入圖片描述

可以看到,修飾counter字段的public、static、volatile關鍵字,在字節碼層面分別是以下訪問標誌: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE

volatile在字節碼層面,就是使用訪問標誌:ACC_VOLATILE來表示,供後續操作此變量時判斷訪問標誌是否爲ACC_VOLATILE,來決定是否遵循volatile的語義處理。

2.3、JVM源碼層面

上小節圖中main方法編譯後的字節碼,有putstaticgetstatic指令(如果是非靜態變量,則對應putfieldgetfield指令)來操作counter字段。那麼對於被volatile變量修飾的字段,是如何實現volatile語義的,從下面的源碼看起。

1、openjdk8根路徑/hotspot/src/share/vm/interpreter路徑下的bytecodeInterpreter.cpp文件中,處理putstaticputfield指令的代碼:

CASE(_putfield):
CASE(_putstatic):
	{
		  // .... 省略若干行 
          // ....
            
		  // Now store the result 現在要開始存儲結果了
          // ConstantPoolCacheEntry* cache; 	-- cache是常量池緩存實例
		  // cache->is_volatile()			    -- 判斷是否有volatile訪問標誌修飾
          int field_offset = cache->f2_as_index();
          if (cache->is_volatile()) { // ****重點判斷邏輯**** 
            // 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) {// byte類型賦值
              obj->release_byte_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ltos) {// long類型賦值
              obj->release_long_field_put(field_offset, STACK_LONG(-1));
            } else if (tos_type == ctos) {// char類型賦值
              obj->release_char_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == stos) {// short類型賦值
              obj->release_short_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ftos) {// float類型賦值
              obj->release_float_field_put(field_offset, STACK_FLOAT(-1));
            } else {// double類型賦值
              obj->release_double_field_put(field_offset, STACK_DOUBLE(-1));
            }
            // *** 寫完值後的storeload屏障 ***
            OrderAccess::storeload();
          } else {
            // 非volatile變量的賦值邏輯
            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));
            }
          }
          UPDATE_PC_AND_TOS_AND_CONTINUE(3, count);
  }

2、重點判斷邏輯cache->is_volatile()方法,調用的是openjdk8根路徑/hotspot/src/share/vm/utilities路徑下的accessFlags.hpp文件中的方法,用來判斷訪問標記是否爲volatile修飾

  // Java access flags
  bool is_public      () const         { return (_flags & JVM_ACC_PUBLIC      ) != 0; }
  bool is_private     () const         { return (_flags & JVM_ACC_PRIVATE     ) != 0; }
  bool is_protected   () const         { return (_flags & JVM_ACC_PROTECTED   ) != 0; }
  bool is_static      () const         { return (_flags & JVM_ACC_STATIC      ) != 0; }
  bool is_final       () const         { return (_flags & JVM_ACC_FINAL       ) != 0; }
  bool is_synchronized() const         { return (_flags & JVM_ACC_SYNCHRONIZED) != 0; }
  bool is_super       () const         { return (_flags & JVM_ACC_SUPER       ) != 0; }
  // 是否volatile修飾
  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; }
  bool is_interface   () const         { return (_flags & JVM_ACC_INTERFACE   ) != 0; }
  bool is_abstract    () const         { return (_flags & JVM_ACC_ABSTRACT    ) != 0; }
  bool is_strict      () const         { return (_flags & JVM_ACC_STRICT      ) != 0; }

3、下面一系列的if…else…對tos_type字段的判斷處理,是針對java基本類型和引用類型的賦值處理。如:

obj->release_byte_field_put(field_offset, STACK_INT(-1));

對byte類型的賦值處理,調用的是openjdk8根路徑/hotspot/src/share/vm/oops路徑下的oop.inline.hpp文件中的方法:

// load操作調用的方法
inline jbyte oopDesc::byte_field_acquire(int offset) const                  
{ return OrderAccess::load_acquire(byte_field_addr(offset));     }
// store操作調用的方法
inline void oopDesc::release_byte_field_put(int offset, jbyte contents)     
{ OrderAccess::release_store(byte_field_addr(offset), contents); }

賦值的操作又被包裝了一層,又調用的OrderAccess::release_store方法。

4、OrderAccess是定義在openjdk8根路徑/hotspot/src/share/vm/runtime路徑下的orderAccess.hpp頭文件下的方法,具體的實現是根據不同的操作系統和不同的cpu架構,有不同的實現。

** 強烈建議大家讀一遍orderAccess.hpp文件中30-240行的註釋!!!**你就會發現本文1.2章所介紹內容的來源,也是網上各種雷同文章的來源。
在這裏插入圖片描述

orderAccess_linux_x86.inline.hpp是linux系統下x86架構的實現:

在這裏插入圖片描述

可以從上面看到,到c++的實現層面,又使用c++中的volatile關鍵字,用來修飾變量,通常用於建立語言級別的memory barrier。在《C++ Programming Language》一書中對volatile修飾詞的解釋:

A volatile specifier is a hint to a compiler that an object may change its value in ways not specified by the language so that aggressive optimizations must be avoided.

含義就是:

  • volatile修飾的類型變量表示可以被某些編譯器未知的因素更改(如:操作系統,硬件或者其他線程等)
  • 使用 volatile 變量時,避免激進的優化。即:系統總是重新從內存讀取數據,即使它前面的指令剛從內存中讀取被緩存,防止出現未知更改和主內存中不一致

5、步驟3中對變量賦完值後,程序又回到了2.3.1小章中第一段代碼中一系列的if…else…對tos_type字段的判斷處理之後。有一行關鍵的代碼:OrderAccess::storeload(); 即:只要volatile變量賦值完成後,都會走這段代碼邏輯。

它依然是聲明在orderAccess.hpp頭文件中,在不同操作系統或cpu架構下有不同的實現。orderAccess_linux_x86.inline.hpp是linux系統下x86架構的實現:

在這裏插入圖片描述

代碼lock; addl $0,0(%%rsp) 其中的addl $0,0(%%rsp) 是把寄存器的值加0,相當於一個空操作(之所以用它,不用空操作專用指令nop,是因爲lock前綴不允許配合nop指令使用)

lock前綴,會保證某個處理器對共享內存(一般是緩存行cacheline,這裏記住緩存行概念,後續重點介紹)的獨佔使用。它將本處理器緩存寫入內存,該寫入操作會引起其他處理器或內核對應的緩存失效。通過獨佔內存、使其他處理器緩存失效,達到了“指令重排序無法越過內存屏障”的作用

2.4、彙編層面

運行2.1章的代碼時,加上JVM的參數:-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly,就可以看到它的彙編輸出。(如果運行報錯,參見上篇文章:synchronized底層原理(從Java對象頭說到即時編譯優化),拉到文章最底部有解決方案)

打印的彙編代碼較長,僅截取其中的關鍵部分:

在這裏插入圖片描述

又看到了lock addl $0x0,(%rsp)指令,熟悉的配方熟悉的味道,和上面2.3章中的步驟5一摸一樣,其實這裏就是步驟5中代碼的體現。

2.5、硬件層面

爲什麼會有上述如此複雜問題?爲什麼會有併發編程?爲什麼會產生可見性、有序性、原子性的線程或內存問題?

歸根結底,還是計算機硬件告訴發展的原因。如果是單核的cpu,肯定不會出現多線程併發的安全問題。正是因爲多核CPU架構,以及CPU緩存才導致一系列的併發問題。

CPU緩存相關內容也是一大塊內容,消化完上述的乾貨內容,請看下一篇對CPU緩存相關的乾貨文章。

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