# Java關鍵字volatile
## 提綱

## 定義
語義上,volatile是表示易變的、不確定的。
功能上,是Java提供的最輕量級的同步機制。
## 前因:從CPU緩存架構類比JMM線程工作內存和主內存關系
要弄懂如何保證可見性的,請看下圖,左側是CPU的緩存架構圖:

如下圖是一些時間參考,可更加直觀的感受到各個組件的訪問速度。

因為CPU的執行速度和內存的讀寫速度,相差太大。
CPU完成操作后,如果要等到內存也執行完成再繼續下一個操作的話,對CPU算力就是極大的浪費。所以為了匹配2者的速度差,引入了高速緩存。現在CPU一般都有3級緩存,其中一級緩存離CPU核最近,速度也最快,可分為指令緩存和數據緩存2部分;下面是二級緩存,一個CPU核心就配備一個一級緩存和二級緩存的,是私有的。而三級緩存則是共享的,再下面是數據總線和主內存。
如下圖是CPU的基本信息:

引入了高速緩存,雖然能讓CPU效率提升,但是也帶來了緩存一致性問題。為了解決這個問題,有兩種方案,一是通過總線鎖實現強一致性;二是緩存一致性協議,目前大多數采用的是MESI緩存一致性協議。(這2個方案都是硬件級別的)
**隨著CPU技術的發展,在CPU硬件級別多是使用的第二種方式,因為鎖住總線期間,其他CPU無法訪問內存,導致性能下降**
而對于Java并發環境下,多線程的共享數據一致性問題也是類似,Java內存模型參考上述的CPU緩存架構實現了自己的線程、工作內存和主內存的關系,如上圖里的右側部分。
### 總線鎖
所謂總線鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那么該處理器可以獨占共享內存,從而保證操作的原子性。
### 緩存一致性協議MESI
MESI協議是當前最主流的緩存一致性協議,在MESI協議中,每個緩存行有4個狀態,可用2個bit表示,它們分別是:
```
Modified(修改):數據有效,數據被修改了,和內存中數據不一致,數據只存在于本Cache中。
Exclusive(獨享):數據有效,數據和內存中的數據一致,數據只存在于本Cache中。
Shared(共享):數據有效,數據和內存中的數據一致,數據存在多個Cache中。
Invalid(無效):數據無效,一旦數據被標記為無效,那效果就等同于它從來沒被加載到緩存中。
```
其詳細狀態轉換如下:

## 特性
因為Java內存模型對volatile關鍵字的支持,使得volatile修飾的變量(實例字段、靜態變量或者數組對象的元素,不包含局部變量,因為局部變量是線程私有的)具備了如下特性:
* **多線程間的可見性**
* **有序性,禁止指令重排序**
* **不保證原子性,如volatile int i=1;i++;**
## volatile底層實現原理
### volatile修飾的底層區別
首先通過如下一段DCL(double check lock)程序來比對一下有volatile和沒有volatile修飾變量的在匯編指令上的區別:
~~~
public class VolatileSingleton {
public static volatile VolatileSingleton instance;
public static VolatileSingleton getInstance(){
if(instance == null){
synchronized(VolatileSingleton.class){
if(instance== null){
instance = new VolatileSingleton();
}
}
}
return instance;
}
public static void main(String[] args) {
VolatileSingleton.getInstance();
}
}
~~~
通過加上如下虛擬機參數,可以只顯示getInstance()方法的匯編指令:
```
# server模式運行
-server
# 讓虛擬機編譯模式執行代碼
-Xcomp
# 使用hsdis來顯示執行的匯編指令,不同平臺的hsdis插件請自行查閱安裝
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintAssembly
# 如下2個命令,只打印關心部分的匯編指令,如果不指定會打印很多其他方法的匯編,造成混亂
# 編譯命令,不要內聯編譯getInstance方法
-XX:CompileCommand=dontinline,*VolatileSingleton.getInstance
# 編譯命令,只編譯getInstance方法
-XX:CompileCommand=compileonly,*VolatileSingleton.getInstance
```
最后將沒有加volatile修飾的匯編指令保存到novolatile.txt,加了volatile的保存到volatile.txt,再使用idea的compare with 對比如下圖:

會發現加了volatile的會多出一行 **lock addl $0x0,(%rsp)** 的匯編指令,這個指令是一個內存屏障。
指令`lock addl $0x0,(%esp)`是一個空操作,關鍵在于 lock 前綴,查詢 IA32 手冊,它的作用是使得本 CPU 的 Cache 寫入了內存,該寫入動作也會引起別的 CPU invalidate 其 Cache。所以通過這樣一個空操作,可讓前面 volatile 變量的修改對其他 CPU 立即可見。
### volatile基于軟內存屏障實現可見性和有序性

通過內存屏障指令lock,如果有修改,處理器會將該變量所在緩存行的數據會寫到主內存,并使得其他CPU里該變量所在的緩存行失效,從而保證該變量的可見性。
而且內存屏障會保證后面的指令不會重排序到屏障前面,從而保證有序性。
#### 可見性定義
對于共享變量a,當線程1修改a的值后,其他線程能立即知道這個修改,就說變量a對所有線程有可見性。
#### 可見性例子
~~~
/**
* volatile 可見性測試
*/
public class VolatileVisibilityTest {
private static volatile boolean ready;
private static int number;
private static class ReaderThread extends Thread{
@Override
public void run() {
while (!ready);
System.out.println(number);
}
}
public static void main(String[] args) throws InterruptedException {
new ReaderThread().start();
Thread.sleep(1000);
number = 42;
ready = true;
Thread.sleep(10000);
}
}
/**
* 因為JMM保證了volatile變量ready的可見性,在main線程中修改為true;
* ReaderThread線程能應用到這個修改,則while(!ready)循環得以跳過。
* 則輸出42,,10秒后退出程序。如果ready沒有修飾為volatile,則沒有可見性,線程Reader會陷入死循環,程序永遠不會停止。
*/
~~~
### volatile不保證原子性
比如復雜操作,i++;
~~~
/**
* volatile不保證原子性
* @Author: mango
* @Date: 2022/7/4 11:32 下午
*/
public class VolatileNoAtomicTest {
private static volatile int number = 0;
static class AdderThread extends Thread{
@Override
public void run() {
for(int i=0;i<10000;i++){
number++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new AdderThread();
t1.start();
Thread t2 = new AdderThread();
t2.start();
t1.join();
t2.join();
System.out.println(number);
}
}
/**
* 結果:
* 有時候輸出小于20000的值,說明number++無法保證原子性
*/
~~~
## volatile優化
追加volatile變量的寬度為操作系統緩存行的寬度,一般為64字節。Java中對象的引用是4字節,`LinkedTransferQueue`會在每個入隊元素的對象引用后填充60個字節,將元素補齊到64字節來提升并發下的入隊和出隊效率。使用追加到64字節的方式來填滿高速緩沖區的緩存行,避免頭接點和尾節點加載到同一個緩存行,使得頭尾節點在修改時不會互相鎖定。
### 不需要補齊到64字節的場景
1. 系統的緩存行不是64字節的,有的是32字節。
2. 共享變量不會被頻繁的寫。
## 參考文檔
* 書籍:葛一鳴 *《Java高并發程序設計第二版》
* 網上文章:https://www.cnblogs.com/zhangxl1016/articles/16001715.html
* 網上文章:https://blog.csdn.net/stackfuture/article/details/122252734
* 網上文章:https://www.cnblogs.com/hbbbs/articles/12116286.html
- 面試突擊
- Java虛擬機
- 認識字節碼
- 000Java發展歷史
- 000Macos10.15.7上編譯OpenJDK8u
- 001熟悉Java內存區域
- 002熟悉HotSpot中的對象
- 003Java如何計算對象大小
- 004垃圾判定算法與4大引用
- 005回收堆和方法區中對象
- 006垃圾收集算法
- 007HotSpot虛擬機垃圾算法實現篇1
- 007HotSpot虛擬機垃圾算法實現篇2
- 007HotSpot虛擬機垃圾算法實現篇3
- 008垃圾收集器
- 009內存分配與回收策略
- 010Java虛擬機相關工具
- 011調優案例分析
- 012一次IDEA的啟動速度調優
- 013類文件Class的結構
- 014熟悉字節碼指令
- 015類加載機制(過程)
- 016類加載器
- IDEA的JVM參數
- Java基礎
- Java自動裝箱與拆箱
- Java基礎數據類型
- Java方法的參數傳遞
- Java并發
- 001走入并行的世界
- 002并行程序基礎
- 003熟悉Java內存模型JMM
- 004Java并發之volatile關鍵字
- 005線程池入門到精通
- 006Java多線程間的同步控制方法
- 007Java維基準測試框架JMH
- 008Java并發容器
- 009Java的線程實現
- 010Java關鍵字synchronized
- 011一些并行模式的熟悉
- 單例模式和不變模式
- 生產者消費者模式
- Future模式
- 012一些并行算法的熟悉
- 面試總結
- 長亮一面