volatile的使用與原子性問題
- 2020 年 3 月 10 日
- 筆記
1. volatile的使用
java源程式碼
public class VolatileUsedClass { private static int sharedVar = 10; public static void main(String[] args) throws Exception { Thread thread1 = new Thread(new Runnable() { @Override public void run() { try { // modify the sharedVar,write first TimeUnit.MICROSECONDS.sleep(500L); sharedVar = 20; System.out.printf("%s modify the shared var to %s ...n", "thread-1", sharedVar); } catch (Exception e) { System.out.println(e); } } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { try { // read the value,read for the last TimeUnit.MICROSECONDS.sleep(505L); System.out.printf("%s read the shared var %s n", "thread-2", sharedVar); } catch (Exception e) { System.out.println(e); } } }); thread2.start(); thread1.start(); thread1.join(); thread2.join(); System.out.println("finish the thread task..."); } }客戶端模式-client
- 不加volatile的執行結果(多次執行)


- 加volatile的執行結果(多次)


- 結果分析
- 在源程式碼中是添加時間休眠主要是保證先寫後讀的邏輯
- 從運行結果可以看出,雖然時間片很短,讀執行緒的數據仍然是本地快取的數據,並沒有從主記憶體中讀取值
- 添加volatile關鍵字之後,可以看到讀執行緒的數據正是寫執行緒之後的數據,也就是寫讀數據是一致的
服務端模式,-server
- 無volatile修飾的執行結果(多次)


- 有volatile修飾的執行結果(多次)

- 結果分析
- 不帶volatile修飾與客戶端執行效果一致
- 但是使用server模式帶有volatile的方式卻出現了數據不一致的情況,為什麼?
- 原因在於-server模式會在編譯成位元組碼的時候進行程式碼重排序導致的,主要用於優化程式提升執行效率
小結
- clinet模式是jdk執行的默認配置,可用於測試環境或者本地開發
- server模式一般用於生產環境,目的是開啟server模式的時候編譯器會針對OS系統情況做一些優化操作
- 服務端與客戶端運行模式參考:
參考JVM Server vs Client Mode: https://javapapers.com/core-java/jvm-server-vs-client-mode/
2. 原子性問題
說明: 以下運行環境是使用-client模式進行,排除重排序的干擾
Java中的原子性
- jdk文檔對於原子性的說明如下:
- 除了long和double類型之外,引用變數與大多數的原始數據類型都具備讀寫操作的原子性
- 所有使用volatile修飾的變數都具備讀寫操作的原子性
- 分析
- 針對64bit的數據類型,主要與處理器(32bit/64bit)有關,在32bit處理器上,JVM會將64bit的long/double劃分為兩個32bit的寫操作,並不具備原子性(數據的讀寫主要是通過處理器匯流排與主記憶體進行傳遞)
- 基於Happen-Before原則,對於volatile的變數讀取總是可以「看到」任何一個執行緒對該volatile變數的最後寫入,因此在臨界區程式碼的執行是具備原子性,即使是long或是double類型
volatile修飾單個變數的自增減問題
- 程式碼
// 部分程式碼,在上述的寫執行緒進行修改, 前提: volatile修飾變數sharedVar Thread t1 = new Thread(){ @Override public void run() { try { // modify the sharedVar TimeUnit.MICROSECONDS.sleep(500L); sharedVar ++; System.out.printf("%s modify the shared var with atomic %s ...n", "thread-1", sharedVar); } catch (Exception e) { System.out.println(e); } } };- 運行結果

- sharedVar = 20L顯示的位元組碼如下:
L3 LINENUMBER 102 L3 BIPUSH 20 // 壓入執行緒的操作數棧 INVOKESTATIC com/xiaokunliu/blogs/thread/volatile2code/VolatileUsedClass.access$002 (I)I //實例化並載入sharedVar並壓入操作數棧,說明完成賦值操作 POP // 彈出數據 L4
- sharedVar ++ 顯示的位元組碼如下:
L3 LINENUMBER 27 L3 INVOKESTATIC com/xiaokunliu/blogs/thread/volatile2code/VolatileUsedClass.access$000 ()I // 實例化並載入sharedVar並壓入操作數棧 ICONST_1 // 將常量 1 壓入執行緒操作數棧總 IADD // 執行sharedVar+1 INVOKESTATIC com/xiaokunliu/blogs/thread/volatile2code/VolatileUsedClass.access$002 (I)I // 重新實力化載入sharedVar,說明完成賦值操作 POP L4
- 運行分析:
- 通過位元組碼可知,sharedVar ++;相比單純賦值操作增加了一個添加的動作
- 也就整體程式碼塊存在在並發多執行緒下交替執行的兩個操作,不具備原子性,volatile在這裡是保證程式碼刷新到主記憶體,對於sharedVar = const 是具備原子性的
使用volatile小結
- 對變數進行寫操作的時候可以通過volatile來實現對其他執行緒的可見,同樣在單步指令操作中是具備原子性,針對long或者double也起到具備原子性的作用
- 對於需要用volatile修飾的變數來完成一系列的非單步操作運算是無法保證原子性,必須藉助lock的方式來實現程式碼塊的原子性
- JDK關於原子性問題說明參考:
參考JDK關於原子性文檔: https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html
感謝花時間閱讀,如有用請轉發或者點個好看,謝謝!

