C++霧中風景13:volatile解惑
筆者入職百度時,二面面試官的讓我聊聊C++之中的 volatile關鍵詞 。volatile在Java和C++之中的差別可謂是 天差地別 ,我只是簡單聊了聊Java之中的 volatile ,面試官對我的回答並不滿意。後續學習 《C++ Prmier》 時,對 volatile 的理解也是雲裡霧裡。入職百度之後,發現身邊的同學時候對volatile也是 誤會頗多 。(果然是“ 面試造核彈,工作擰螺絲 ”)所以筆者花了一些時間,整理了這篇文章,希望各位C++程式設計師能徹底釐清 volatile 。
1.volatile的誤會
volatile這個單詞在英文之中的意思是: 易變的,不穩定的 的含義。所以顧名思義,一旦變數通過了 volatile關鍵詞 修飾之後,說明變數是 易變的和不穩定的 。而C++之中最大的誤會就是認為 volatile關鍵詞 與併發程式設計有關,至於為何會引起這樣的誤會呢?筆者覺得罪魁禍首可能是下面的原因:
Java中的volatile
volatile影響最為深遠的就是Java之中的功用,筆者第一次接觸這個關鍵詞也是在Java之中。( 加上數目龐大的Java程式設計師~~ )Java之中 volatile 的效果是:
- 確保記憶體可見性
讀和寫一個 volatile變數 具有 全域性有序性 。每個執行緒訪問一個 volatile變數 時都會讀取它在記憶體之中的當前值,而不是使用一個快取中的值。 這樣能夠確保當某執行緒對volatile變數進行了修改後,後面執行的其他執行緒能看到volatile變數的變動。 所以在簡單的多執行緒程式之中,volatile變數可以起到輕量級的同步作用。但是 volatile關鍵字 並不保證執行緒讀寫變數的相對順序,所以適用場景有限。 - 禁止指令重排序
指令重排序是JVM 為了提高程式的執行效率,在不影響單執行緒程式執行結果的前提下,對各種指令執行的過程進行重新排序和優化,來增加程式處理時的並行度。 指令重排序在單執行緒下能夠保證正確,但是在多執行緒的環境下就很難確保指令重排序的語義正確。
JDK5引入concurrent包中atomic,JDK6將synchronized關鍵字的效能優化後。絕大多數場景,筆者都不再推薦使用volatile這個關鍵字了。
MSVC 微軟的鍋
早期的 MSVC之中 volatile 具有 Release 和 Acquire 語義,這我想給許多 C++程式猿造成了誤解。後續微軟將這個關鍵字做了一個切換: volatile:ms ,用加 ms 的修飾來延續之前的語義。

2.volatile 的作用
其實上一節對 volatile 的誤用做了討論。接下來筆者來帶大家看看,實際 volatile 關鍵字到底起到怎麼樣的作用。先看如下的程式碼:
int n = 10; int main() { int a = n; int b = n; return 0; }
:

接下來,我們將變數新增上
volatile**關鍵字,看看會出現什麼效果:int n = 10; int main() { volatile int a = n; volatile int b = n; return 0; }
重新編譯這部分程式碼轉換為彙編程式碼,我們來看看:

看到這部分彙編程式碼,想必各位應該對 volatile關鍵詞 的作用應該心中有數了。 volatile相當於顯式的要求編譯器禁止對 volatile 變數進行優化,並且要求每個變數賦值時,需要顯式從暫存器%eax拷貝。volatile 關鍵字在嵌入式程式設計之中會需要用到,在特定環境下,暫存器的變數可能會發生變化。volatile 所以聲明瞭暫存器部分的資料是『易變的』,需要防止編譯器優化變數,強制載入暫存器。
但是如果需要實現類似 Java 之中 volatile 的效果呢?可以在程式碼之中顯式插入記憶體屏障,讓 CPU 強制重新整理暫存器的變數到記憶體之中。
int n = 10; int main() { volatile int a = n; asm volatile("" ::: "memory"); volatile int b = n; return 0; }
現在我們再來看看編譯生成的彙編程式碼:

由上述的彙編程式碼我們可以看到,在添加了記憶體屏障之後,對變數的賦值操作需要顯式的記憶體之中取值。實際 Java 在實現 volatile 關鍵字時,也是通過上述語句來實現的。