【Java多執行緒】volatile關鍵字解析(五)
一、volatile是什麼
volatile在java語言中是一個關鍵字,用於修飾變數。被volatile修飾的變數後,表示這個變數在不同執行緒中是共享,編譯器與執行時都會注意到這個變數是共享的,因此不會對該變數進行重排序。
volatile關鍵字的兩層語義
一旦一個共享變數(類的成員變數、類的靜態成員變數)被volatile修飾之後,那麼就具備了兩層語義:
1)保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的。
2)禁止進行指令重排序。
語義1、可見性變數的共享
示例
1 public class TestVolatile {2 3 static boolean found = false; 4 5 public static void main(String[] args) { 6 7 new Thread(new Runnable() { 8 public void run() { 9 System.out.println(Thread.currentThread().getName() + ":等基友送筆來..."); 10 11 while (!found) { 12} 13 14 System.out.println(Thread.currentThread().getName() + ":筆來了,開始寫字..."); 15 } 16 }, "我的執行緒").start(); 17 18 new Thread(new Runnable() { 19 public void run() { 20 try { 21 Thread.sleep(2000);22 } catch (InterruptedException e) { 23 e.printStackTrace(); 24 } 25 26 System.out.println(Thread.currentThread().getName() + ":基友找到筆了,送過去..."); 27 found = true; 28 } 29 }, "基友執行緒").start(); 30 } 31 }
上面的程式碼是一種典型用法,檢查某個標記(found)的狀態判斷是否退出迴圈。但是上面的程式碼有可能會結束,也可能永遠不會結束。因為每一個執行緒都擁有自己的工作記憶體,當一個執行緒讀取變數的時候,會把變數在自己記憶體中拷貝一份。之後訪問該變數的時候都通過訪問執行緒的工作記憶體,如果修改該變數,則將工作記憶體中的變數修改,然後再更新到主存上。這種機制讓程式可以更快的執行,然而也會遇到像上述例子這樣的情況。
存在一種情況,found變數被分別拷貝到我的執行緒、基友執行緒兩個執行緒中,此時found為false。基友執行緒開始迴圈,我的執行緒修改本地found變數稱為true,並將found=true回寫到主存,但是found已經在基友執行緒執行緒中拷貝過一份,基友執行緒迴圈時候讀取的是基友執行緒 工作記憶體中的found變數,而這個found始終是false,程式死迴圈。我們稱基友執行緒對我的執行緒更新found變數的行為是不可見的。
如果found變數通過volatile進行修飾,基友執行緒修改found變數後,會立即將變量回寫到主存中,並將我的執行緒裡的found失效。我的執行緒發現自己變數失效後,會重新去主存中訪問found變數,而此時的found變數已經變成true。迴圈退出。
static volatile boolean found= false;
語義2、禁止指令衝排序:
示例
1 public class ReorderTest { 2 3 private static int x = 0, y = 0; 4 private static int a = 0, b = 0; 5 6 public static void main(String[] args) throws InterruptedException { 7 int i = 0; 8 for (;;){ 9 i++; 10 x = 0; y = 0; 11 a = 0; b = 0; 12 Thread t1 = new Thread(new Runnable() { 13 public void run() { 14 a = 1; 15 x = b; 16 } 17 }); 18 19 Thread t2 = new Thread(new Runnable() { 20 public void run() { 21 b = 1; 22 y = a; 23 } 24 }); 25 26 t1.start(); 27 t2.start(); 28 t1.join(); 29 t2.join(); 30 31 // 只有重排序的情況下,才會出現 0,0的結果 32 // 即x = b , y = a 比 a = 1,b = 1 先執行的情況下才會出現 33 if(x == 0 && y == 0) { 34 String result = "第" + i + "次\n x=" + x + ", y=" + y + ", a=" + a + ", b=" + b; 35 System.out.println(result); 36 break; 37 } 38 } 39 } 40 }
執行結果:第108861次 x=0, y=0, a=1, b=1
上面的程式碼要出現x == 0 && y == 0的情況,只有保證x = b 比b = 1先執行,y = a 比a = 1 先執行,才會出現,圖解如下:
那麼t2中的程式碼放生了重排序,即指令重排序。如果加上volatile修飾 x、y、a、b變數之後,如下:
private volatilestatic int x = 0, y = 0;
private volatilestatic int a = 0, b = 0;
程式永遠不會結束,因為volatile禁止了指令重排序
volatile不保證原子性
示例
1 public class AtomicTest { 2 3 private volatile static int counter = 0; 4 5 public static void main(String[] args) { 6 7 for (int i = 0; i < 10; i++) { 8 Thread thread = new Thread(()->{ 9 for (int j = 0; j < 10000; j++) { 10 counter++; 11 } 12 System.out.println(Thread.currentThread().getName() + " Over~~~"); 13 }); 14 thread.start(); 15 } 16 17 try { 18 Thread.sleep(5000); 19 } catch (InterruptedException e) { 20 e.printStackTrace(); 21 } 22 23 System.out.println(counter); 24 25 } 26 }
重執行結果來看,它結果不等於100000,說明volatile不保證原子性
二、volatile的的底層實現
2.1、 Java程式碼層面
上一段最簡單的程式碼,volatile
用來修飾Java變數
1 public class TestVolatile { 2 3 public static volatile int counter = 1; 4 5 public static void main(String[] args){ 6 counter = 2; 7 System.out.println(counter); 8 } 9 10 }
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
指令的程式碼:
1 CASE(_putfield): 2 CASE(_putstatic): 3 { 4 // .... 省略若干行 5 // .... 6 7 // Now store the result 現在要開始儲存結果了 8 // ConstantPoolCacheEntry* cache; -- cache是常量池快取例項 9 // cache->is_volatile() -- 判斷是否有volatile訪問標誌修飾 10 int field_offset = cache->f2_as_index(); 11 if (cache->is_volatile()) { // ****重點判斷邏輯**** 12 // volatile變數的賦值邏輯 13 if (tos_type == itos) { 14 obj->release_int_field_put(field_offset, STACK_INT(-1)); 15 } else if (tos_type == atos) {// 物件型別賦值 16 VERIFY_OOP(STACK_OBJECT(-1)); 17 obj->release_obj_field_put(field_offset, STACK_OBJECT(-1)); 18 OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0); 19 } else if (tos_type == btos) {// byte型別賦值 20 obj->release_byte_field_put(field_offset, STACK_INT(-1)); 21 } else if (tos_type == ltos) {// long型別賦值 22 obj->release_long_field_put(field_offset, STACK_LONG(-1)); 23 } else if (tos_type == ctos) {// char型別賦值 24 obj->release_char_field_put(field_offset, STACK_INT(-1)); 25 } else if (tos_type == stos) {// short型別賦值 26 obj->release_short_field_put(field_offset, STACK_INT(-1)); 27 } else if (tos_type == ftos) {// float型別賦值 28 obj->release_float_field_put(field_offset, STACK_FLOAT(-1)); 29 } else {// double型別賦值 30 obj->release_double_field_put(field_offset, STACK_DOUBLE(-1)); 31 } 32 // *** 寫完值後的storeload屏障 *** 33 OrderAccess::storeload(); 34 } else { 35 // 非volatile變數的賦值邏輯 36 if (tos_type == itos) { 37 obj->int_field_put(field_offset, STACK_INT(-1)); 38 } else if (tos_type == atos) { 39 VERIFY_OOP(STACK_OBJECT(-1)); 40 obj->obj_field_put(field_offset, STACK_OBJECT(-1)); 41 OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0); 42 } else if (tos_type == btos) { 43 obj->byte_field_put(field_offset, STACK_INT(-1)); 44 } else if (tos_type == ltos) { 45 obj->long_field_put(field_offset, STACK_LONG(-1)); 46 } else if (tos_type == ctos) { 47 obj->char_field_put(field_offset, STACK_INT(-1)); 48 } else if (tos_type == stos) { 49 obj->short_field_put(field_offset, STACK_INT(-1)); 50 } else if (tos_type == ftos) { 51 obj->float_field_put(field_offset, STACK_FLOAT(-1)); 52 } else { 53 obj->double_field_put(field_offset, STACK_DOUBLE(-1)); 54 } 55 } 56 UPDATE_PC_AND_TOS_AND_CONTINUE(3, count); 57 }
2、重點判斷邏輯cache->is_volatile()
方法,呼叫的是openjdk8根路徑/hotspot/src/share/vm/utilities
路徑下的accessFlags.hpp
檔案中的方法,用來判斷訪問標記是否為volatile修飾
1 // Java access flags 2 bool is_public () const { return (_flags & JVM_ACC_PUBLIC ) != 0; } 3 bool is_private () const { return (_flags & JVM_ACC_PRIVATE ) != 0; } 4 bool is_protected () const { return (_flags & JVM_ACC_PROTECTED ) != 0; } 5 bool is_static () const { return (_flags & JVM_ACC_STATIC ) != 0; } 6 bool is_final () const { return (_flags & JVM_ACC_FINAL ) != 0; } 7 bool is_synchronized() const { return (_flags & JVM_ACC_SYNCHRONIZED) != 0; } 8 bool is_super () const { return (_flags & JVM_ACC_SUPER ) != 0; } 9 // 是否volatile修飾 10 bool is_volatile () const { return (_flags & JVM_ACC_VOLATILE ) != 0; } 11 bool is_transient () const { return (_flags & JVM_ACC_TRANSIENT ) != 0; } 12 bool is_native () const { return (_flags & JVM_ACC_NATIVE ) != 0; } 13 bool is_interface () const { return (_flags & JVM_ACC_INTERFACE ) != 0; } 14 bool is_abstract () const { return (_flags & JVM_ACC_ABSTRACT ) != 0; } 15 bool is_strict () const { return (_flags & JVM_ACC_STRICT
3、下面一系列的if...else...對tos_type
欄位的判斷處理,是針對java基本型別和引用型別的賦值處理。如:
1 obj->release_byte_field_put(field_offset, STACK_INT(-1));
對byte型別的賦值處理,呼叫的是openjdk8根路徑/hotspot/src/share/vm/oops
路徑下的oop.inline.hpp
檔案中的方法:
1 // load操作呼叫的方法 2 inline jbyte oopDesc::byte_field_acquire(int offset) const 3 { return OrderAccess::load_acquire(byte_field_addr(offset)); } 4 // store操作呼叫的方法 5 inline void oopDesc::release_byte_field_put(int offset, jbyte contents) 6 { 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,這裡記住快取行概念,後續重點介紹)的獨佔使用。它將本處理器快取寫入記憶體,該寫入操作會引起其他處理器或核心對應的快取失效。通過獨佔記憶體、使其他處理器快取失效,達到了“指令重排序無法越過記憶體屏障”的作用
記憶體屏障參考:【Java多執行緒】JMM(Java記憶體模型)(四)
本文參考:https://zhuanlan.zhihu.com/p/133851347