Java 面試之你需要知道的多線程 volatile 關鍵字

前言

Java 面試之你需要知道的多線程 volatile 關鍵字

在高併發實際場景下,volatile 關鍵字應用較多,同時,這也是面試的必問的一個知識點了。那麼,valatile 關鍵字有什麼作用呢?

內存可見性

這需要從 Java 的內存模型(JMM)說起,JMM 規定所有的變量都需要存放在主內存中,同時,每個線程又有著自己的工作內存 (主要做高速緩存)。

這樣,線程工作時需要操作變量時,需要將主內存中的數據拷貝到工作內存中。這樣線程對數據的任何操作都是基於線程本身的工作內存(目的是為了提升效率),而不能去直接操作主內存以及其他線程的工作內存的數據,當線程對數據做了更新以後,還需要將更新後的數據刷新到主內存中。

Java 面試之你需要知道的多線程 volatile 關鍵字

以上圖為例,在併發情況下,可能會出現線程 B 讀取到的數據是線程 A 更新之前的數據,從而導致數據不一致。

這個時候,就需要 volatile 出場了:

當一個變量被 volatile 修飾的時候,任何線程對其做的寫操作都會被立即刷新到主內存中,並且強制讓那些緩存了該變量的線程內的該變量數據清空,需要從主內存中重新讀取最新數據

注意:volatile 修飾的變量,並不是讓線程直接操作主內存獲取數據,還是需要將變量拷貝到工作內存中。

volidate 應用 demo

我們模擬一個簡單的應用場景,兩個線程需要同時訪問主內存中的某個標誌位變量 flag, 我們用 volidate 來修飾:

Java 面試之你需要知道的多線程 volatile 關鍵字

主線程在對 flag 做了修改以後,flag 會立馬被刷新到主內存中,從而及時停止線程 A, 如果說 flag 沒有被 volidate 修飾,就可能會出現延遲。

volidate 一定能保證線程的安全性嗎?

上面說到的 volidate 能夠保證被修改的變量及時被刷新到主內存中,很多人會認為這樣就能保證線程的安全性。

答案是否定的。volidate 並不能保證線程的安全性!

Java 面試之你需要知道的多線程 volatile 關鍵字

上面這段代碼中,三個線程對 int i 進行累加,最終的結果都會小於 30000,而不是剛好 30000!

這是什麼情況?不是說 volidate 保證了內存的可見性嗎?

volidate 固然保證了內存的可見性,使得每個線程都能拿到最新的值,但是 count ++ 這個操作並不是原子的,看似簡單的自增加 1 的操作,實際上包含了三個操作:

  • 獲取值;
  • 自增;
  • 賦值;

但這三個操作沒有原子性,並不能同時完成。

那要如何解決對沒有原子性的基本數據的併發安全性呢?

  • 1.用單線程串行執行(不推薦,無法充分發揮多核 CPU 的優勢);
  • 2.通過 synchronize 對數據上鎖保證原子性;
  • 3.通過 Atomic 包中的 AtomicInteger 來替換 int, 它的底層利用了 CAS 算法來保證原子性

validate 之指令重排

validate 除了能夠保證內存的可見性,它的另一重作用是防止 JVM 進行指令重排優化。

用代碼說事:

Java 面試之你需要知道的多線程 volatile 關鍵字

上面是一段基礎代碼,理想情況下它的執行順序是:1 ==> 2 ==> 3。但是在 JVM 對其進行指令重排優化後,它的執行順序可能變為: 2 ==> 1 ==> 3.

JVM 的指令重排優化是在保證最終結果不變的情況下進行的。

上面的代碼還看不出指令重排對實際業務帶來的影響,看下面的代碼:

Java 面試之你需要知道的多線程 volatile 關鍵字

如果上面的 flag 變量沒被 volidate 修飾的話,JVM 指令重排優化後,導致 value 還沒有被初始化,就有可能被線程 B 使用了。

這裡加上 volidate 之後可以保證業務的正確性。

volidate 防指令重排應用

比較經典的應用場景就是雙重懶加載的單例模式了:

Java 面試之你需要知道的多線程 volatile 關鍵字

上面對 Singleton 對象添加 volatile 關鍵字,就是為了防止指令重排。

如果說我們不使用的話,singleton = new Singleton();,這段代碼其實是分為三步:

  • 第一步:分配內存空間
  • 第二步:初始化對象
  • 第三步:將 singleton 對象指向分配的內存地址

加上 volidate 保證上面三步得以順序執行,否則可能出現 第二步 在 第三步 之前執行的情況發生,就可能導致某個線程拿到的單例對象是還沒有被初始化,導致程序報錯。

總結

volatile 在 Java 併發應用場景有很多,比如像 Atomic 包中的 value、以及 AbstractQueuedLongSynchronizer 中的 state 都是被定義為 volatile 來用於保證內存可見性。

將這塊理解透徹對我們編寫併發程序時將獲益匪淺。


同時也歡迎大家關注下面我的微信公眾號哦 ~~~

Java技術說


分享到:


相關文章: