volatile的特性
volatile是Java中用於修飾變量的關鍵字,其主要是保證了該變量的可見性以及順序性,但是沒有保證原子性;其是Java中最為輕量級的同步關鍵字;
接下來我將會一步步來分析volatile關鍵字是如何在Java代碼層面、字節碼層面、JVM源碼層次、匯編層面、操作系統層面、CPU層面來保證可見性和順序性的;
Java代碼層面
當一個變量被定義為volatile之后,具備兩項特性:
- 保證此變量對所有線程的可見性
- 禁止指令重排序優化
volatile所保證的可見性
volatile所修飾的變量在一條線程修改一個變量的值的時候,新值對於其他線程來說是可以立即知道的;
普通變量的值在線程間傳遞的時候都是通過主內存去完成;

根據JMM我們可以知道,每一個線程其實都有它單獨的棧空間,而實際的對象其實都是存放在主內存中的,所以如果是普通對象的話,便會有一個棧空間的對象與主內存中的對象存在差異的時間;而volatile所修飾的變量其保持了可見性,其會強制讓棧空間所存在的對應變量失效,然后從主內存強制刷新到棧空間,如此便每次看到的都是最新的數據;
volatile所保證的禁止指令重排
Java的每一行語句其實都對應了一行或者多行字節碼語句,而每一行字節碼語句又對應了一行或者多行匯編語句,而每一行匯編語句又對應了一行或者多行機器指令;但是CPU的指令優化器可能會對其指令順序進行重排,優化其運行效率,但是這樣也可能會導致並發問題;而volatile便可以強制禁止優化指令重排;
volatile在字節碼層面的運用
我們先看到以下代碼
點擊查看代碼
public class Main {
static int a ;
static volatile int b ;
public static synchronized void change(int num) {
num = 0;
}
public static void main(String[] args) {
a = 10;
b = 20;
change(a);
}
}
我們先試用javac來將java文件編譯為class文件,然后通過javap -v來反編譯;
點擊查看代碼
Classfile /opt/software/java-study/Main.class
Last modified Mar 1, 2022; size 400 bytes
MD5 checksum c7691713c9365588495a60da768c32a6
Compiled from "Main.java"
public class Main
SourceFile: "Main.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#21 // Main.a:I
#3 = Fieldref #5.#22 // Main.b:I
#4 = Methodref #5.#23 // Main.change:(I)V
#5 = Class #24 // Main
#6 = Class #25 // java/lang/Object
#7 = Utf8 a
#8 = Utf8 I
#9 = Utf8 b
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 change
#15 = Utf8 (I)V
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 SourceFile
#19 = Utf8 Main.java
#20 = NameAndType #10:#11 // "<init>":()V
#21 = NameAndType #7:#8 // a:I
#22 = NameAndType #9:#8 // b:I
#23 = NameAndType #14:#15 // change:(I)V
#24 = Utf8 Main
#25 = Utf8 java/lang/Object
{
static int a;
flags: ACC_STATIC
static volatile int b;
flags: ACC_STATIC, ACC_VOLATILE
public Main();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static synchronized void change(int);
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=1, locals=1, args_size=1
0: iconst_0
1: istore_0
2: return
LineNumberTable:
line 5: 0
line 6: 2
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: bipush 10
2: putstatic #2 // Field a:I
5: bipush 20
7: putstatic #3 // Field b:I
10: getstatic #2 // Field a:I
13: invokestatic #4 // Method change:(I)V
16: return
LineNumberTable:
line 9: 0
line 10: 5
line 11: 10
line 12: 16
}
volatile在JVM源碼方面的運用
在JVM源碼方面,我編譯了OpenJDK7然后利用find與grep進行全局查找,然后進行方法追蹤,由於涉及到大量C++的知識,我便跳過其C++代碼追蹤,而直接看最后追蹤到的函數;
先來做一個總結,其實volatile的JVM源碼的原理對應的是被稱為內存屏障來實現的;
點擊查看代碼
static void loadload();
static void storestore();
static void loadstore();
static void storeload();
- LoadLoad屏障:(指令Load1; LoadLoad; Load2),在Load2及后續讀取操作要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。
- LoadStore屏障:(指令Load1; LoadStore; Store2),在Store2及后續寫入操作被刷出前,保證Load1要讀取的數據被讀取完畢。
- StoreStore屏障:(指令Store1; StoreStore; Store2),在Store2及后續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。
- StoreLoad屏障:(指令Store1; StoreLoad; Load2),在Load2及后續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的。在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種內存屏障的功能
對於volatile操作而言,其操作步驟如下:
- 每個volatile寫入之前,插入一個StoreStore,寫入以后插入一個StoreLoad
- 每個volatile讀取之前,插入一個LoadLoad,讀取之后插入一個LoadStore
在JVM源碼層次而言,內存屏障直接起到了禁止指令重排的作用,且之后與總線鎖或者MESI協議配合實現了可見性;
匯編層次
在匯編層次而言,我是使用JITWatch配合hsdis進行的轉匯編,可以發現在含有volatile的變量的時候,匯編指令會有一個lock前綴,而lock前綴在CPU層次中自己實現了內存屏障的功能;
CPU層次
在x86的架構中,含有lock前綴的指令擁有兩種方法實現;
一種是開銷很大的總線鎖,它會把對應的總線直接全部鎖住,如此明顯是不合理的;
所以后期intel引入了緩存鎖以及mesi協議,如此便可以輕量化的實現內存屏障;
