## 16 讓你眼見為實—volatile詳解
> 人要有毅力,否則將一事無成。
> ——居里夫人
上一節我們講解了 Atomic 變量。Atomic 以更為輕量的方式實現原子性。不過也存在其局限性,只能應用于特定的場景。本節我們將講解的 volatile 關鍵字,則是用來解決可見性、有序性問題。上一章講解可見性問題時,已經簡單提到過 volatile 關鍵字。被 volatile 關鍵字修飾的變量,會確保值的變化被其它線程所感知,從而從主存中取得該變量最新的值。此外,在 happans-before 原則中有一條 volatile 變量原則,闡述了 vlatile 如何確保有序性。
## 1\. volatile 效果
我們先通過之前的例子來回顧下 volatile 的作用,例子很簡單,主線程試圖通過修改 flag 的值,來觸發 visableThread 線程打印自己線程 name。代碼如下:
~~~java
private static class ShowVisibility implements Runnable{
public static Object o = new Object();
private volatile Boolean flag = false;
@Override
public void run() {
while (true) {
if (flag) {
System.out.println(Thread.currentThread().getName()+":"+flag);
}
}
}
}
public static void main(String[] args) throws InterruptedException {
ShowVisibility showVisibility = new ShowVisibility();
Thread visableThread = new Thread(showVisibility);
visableThread.start();
//給線程啟動的時間
Thread.sleep(500);
//更新flay
showVisibility.flag=true;
System.out.println("flag is true, thread should print");
Thread.sleep(1000);
System.out.println("I have slept 1 seconds. Is there anything printed ?");
}
}
~~~
代碼中使用 volatile 修飾 flag 變量。這確保在多個線程并發時,任何一個線程改變了 flag 的值都會立即被其它線程所看到。以上程序 main 線程修改了 flag 值后,visableThread 能夠立即打印出自己的線程 name。但如果我們把 flag 前的 volatile 去掉,可以看到 main 線程修改了 flag 值后,visableThread 也不會有任何輸出。也就是說 visableThread 并不知道 flag 值已經被修改。
原因在之前文章中也已經分析過,為了提高計算效率,CPU 會從緩存中取得 flag 值。但是主存中 flag 值的變化,visableThread 線程并不知道,導致其緩存和主存不一致,獲取到的是失效的 flag 值。
## 2\. 理解 volatile
volatile 關鍵字可以用來修飾實例變量和類變量。被 volatile 修飾后,該變量或獲得以下特性:
1. 可見性。任何線程對其修改,其它線程馬上就能讀到最新值;
2. 有序性。禁止指令重排序。

之前章節我們講解過,CPU 為了提升速度,采用了緩存,因此造成了多個線程緩存不一致的問題,這也是可見性的根源。為了解決緩存一致性,我們需要了解緩存一致性協議。MESI 協議是目前主流的緩存一致性協議。此協議會保證,寫操作發生時,線程獨占該變量的緩存,CPU 并且會通知其它線程對于該變量所在的緩存段失效。只有在獨占操縱完成之后,該線程才能修改此變量。而此時由于其它緩存全部失效,所以就不存在緩存一致性問題。而其它線程的讀取操作,需要等寫入操作完成,恢復到共享狀態。
volatile 是如何做到以上機制的呢?我們可以看一下對 volatile 修飾變量的賦值操作,編譯成指令后的代碼:
~~~assembly
mov 0x20(%rsp),%rsi
mov %rax,%r10
shr $0x3,%r10
mov %r10d,0xc(%rsi)
shr $0x9,%rsi
movabs $0x7f55dd1cb000,%rdi
movb $0x0,(%rsi,%rdi,1)
lock addl $0x0,(%rsp)
~~~
我們不需要理解以上指令,只需要關注最后一行。可以看到最后一行使用了 lock 關鍵字。lock 的作用是在其有效的范圍內鎖住總線,從而執行該行代碼線程所在的處理器能夠獨占資源。由于總線被鎖定,開銷很大的。所以新的 CPU 實現已經不會鎖住總線,而是鎖定變量所在的緩存區域,就像上文描述的 MESI 協議,從而保證了數據的可見性。
volatile 的有序性則是通過內存屏障。所謂的內存屏障就是在屏障前的所有指令可以重排序的,屏障之后的指令也可以重排序,但是重排序的時候不能越過內存屏障。也就是說內存屏障前的指令不會被重排序到內存屏障之后,反之亦然。
volatile 能夠保證變量的可見性和有序性,但是并不能保證原子性。比如我們用 volatile 修飾了變量 i,多線程并發執行 i++。假如有 10 個線程,每個線程執行 1 萬次 i++,那么最后 i 的結果肯定不是 10 萬。因為 i++ 實際為三步操作:
1. 從主存取得 i 的值,存入緩存;
2. 為 i 加 1;
3. 賦給 i,寫入主存。
這三步在沒有原子性保證時多線程并發,就會導致不同線程同時執行了步驟 1,讀取到了一樣的 n 值,從而造成了重復的 +1 操作。多次 i++ 操作但只為 i 增加了 1。從試驗結果可以明顯的看出 volatile 并不會保證原子性。
## 3\. volatile 的使用場景
了解 volatile 原理之后,我們總結一下 volatile 的特性和局限性。
volatile 能為我們提供如下特性:
1. 確保實例變量和類變量的可見性;
2. 確保 volatile 變量前后代碼的重排序以 volatile 變量為界限。
volatile 的局限性:
1. volatile 的可見性和有序性只能作用于單一變量;
2. volatile 不能確保原子性;
3. volatile 不能作用于方法,只能修飾實例或者類變量。
volatile 的以上特點,決定了它的使用場景是有限的,并不能完全取代 synchronized 同步方式。一般使用 volatile 的場景是代碼中通過某個狀態值 flag 做判斷,flag 可能被多個線程修改。如果不使用 volatile 修飾,那么 flag 不能保證最新的值被每個線程讀取到。而在使用 volatile 修飾后,任何線程對 flag 的修改,都立刻對其它線程可見。此外其它線程看到 flag 變化時,所有對 flag 操作前的代碼都已生效,這是 volatile 的有序性確保的。
正是由于 volatile 有如上局限性,所以我們只能在上述場景或者其它適合的場景使用 volatile。反推 volatile 不適用的場景如下:
1. 一個變量或者多個變量的原子性操作;
2. 不以 volatile 變量操作作為分界線的有序性保證。
volatile 無法解決的問題最終還得通過 sychronized 或者其它加鎖方式來確保同步。
## 4\. 總結
本節我們深入講解了 volatile 關鍵字。不但學習了 volatile 的特性和原理,并了解了 volatile 的局限性。我們在開發中最常用的用法,是使用 volatile 修飾作為標識判斷的變量。以確保任何線程對它的修改都能立即被其它線程看到,從而正確觸發判斷邏輯。volatile 可以在特定的場景下高效解決并發問題。不過由于自身的局限性,很多時候還是需要依靠 synchronized 或它加鎖方式來實現同步。下一節我們就來看看一直被提及的 synchronized 關鍵字如何使用及其原理。
}
- 前言
- 第1章 Java并發簡介
- 01 開篇詞:多線程為什么是你必需要掌握的知識
- 02 絕對不僅僅是為了面試—我們為什么需要學習多線程
- 03 多線程開發如此簡單—Java中如何編寫多線程程序
- 04 人多力量未必大—并發可能會遇到的問題
- 第2章 Java中如何編寫多線程
- 05 看若兄弟,實如父子—Thread和Runnable詳解
- 06 線程什么時候開始真正執行?—線程的狀態詳解
- 07 深入Thread類—線程API精講
- 08 集體協作,什么最重要?溝通!—線程的等待和通知
- 09 使用多線程實現分工、解耦、緩沖—生產者、消費者實戰
- 第3章 并發的問題和原因詳解
- 10 有福同享,有難同當—原子性
- 11 眼見不實—可見性
- 12 什么?還有這種操作!—有序性
- 13 問題的根源—Java內存模型簡介
- 14 僵持不下—死鎖詳解
- 第4章 如何解決并發問題
- 15 原子性輕量級實現—深入理解Atomic與CAS
- 16 讓你眼見為實—volatile詳解
- 17 資源有限,請排隊等候—Synchronized使用、原理及缺陷
- 18 線程作用域內共享變量—深入解析ThreadLocal
- 第5章 線程池
- 19 自己動手豐衣足食—簡單線程池實現
- 20 其實不用造輪子—Executor框架詳解
- 第6章 主要并發工具類
- 21 更高級的鎖—深入解析Lock
- 22 到底哪把鎖更適合你?—synchronized與ReentrantLock對比
- 23 按需上鎖—ReadWriteLock詳解
- 24 經典并發容器,多線程面試必備—深入解析ConcurrentHashMap上
- 25 經典并發容器,多線程面試必備—深入解析ConcurrentHashMap下
- 26不讓我進門,我就在門口一直等!—BlockingQueue和ArrayBlockingQueue
- 27 倒數計時開始,三、二、一—CountDownLatch詳解
- 28 人齊了,一起行動—CyclicBarrier詳解
- 29 一手交錢,一手交貨—Exchanger詳解
- 30 限量供應,不好意思您來晚了—Semaphore詳解
- 第7章 高級并發工具類及并發設計模式
- 31 憑票取餐—Future模式詳解
- 32 請按到場順序發言—Completion Service詳解
- 33 分階段執行你的任務-學習使用Phaser運行多階段任務
- 34 誰都不能偷懶-通過 CompletableFuture 組裝你的異步計算單元
- 35 拆分你的任務—學習使用Fork/Join框架
- 36 為多線程們安排一位經理—Master/Slave模式詳解
- 第8章 總結
- 37 結束語