乾貨長文預警!!!
文章目錄:
一、volatile的作用
1.1、volatile變數的可見性
1.2、volatile變數的禁止指令重排序
二、volatile的的底層實現
2.1、 Java程式碼層面
2.2、位元組碼層面
2.3、JVM原始碼層面
2.4、彙編層面
2.5、硬體層面
volatile
關鍵字是Java虛擬機器提供的最輕量級的同步機制。在多執行緒程式設計中volatile和synchronized都起著舉足輕重的作用,沒有這兩者,也就沒有那麼多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程式碼、位元組碼、JVM原始碼、彙編層面、硬體層面
去揭開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方法編譯後的位元組碼,有putstatic和getstatic指令(如果是非靜態變數,則對應putfield和getfield指令)來操作counter欄位。那麼對於被volatile變數修飾的欄位,是如何實現volatile語義的,從下面的原始碼看起。
1、openjdk8根路徑/hotspot/src/share/vm/interpreter路徑下的bytecodeInterpreter。cpp檔案中,處理putstatic和putfield指令的程式碼:
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快取相關的乾貨文章。