1. 程式人生 > 實用技巧 >【Java多執行緒】volatile關鍵字解析(五)

【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方法編譯後的位元組碼,有putstaticgetstatic指令(如果是非靜態變數,則對應putfieldgetfield指令)來操作counter欄位。那麼對於被volatile變數修飾的欄位,是如何實現volatile語義的,從下面的原始碼看起。

  1、openjdk8根路徑/hotspot/src/share/vm/interpreter路徑下的bytecodeInterpreter.cpp檔案中,處理putstaticputfield指令的程式碼:

 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