volatile
關鍵字雖然從字面上理解起來比較簡單,可是要用好不是一件容易的事情。本文咱們就從JVM內存模型開始,瞭解一下volatile
的應用場景。java
在瞭解volatile
以前,咱們有必要對JVM的內存模型有一個基本的瞭解。Java的內存模型規定了全部的變量都存儲在主內存中(即物理硬件的內存),每條線程還具備本身的工做內存(工做內存可能位於處理器的高速緩存之中),線程的工做內存中保存了該線程使用到的變量的主內存副本拷貝,線程對變量的全部操做(讀取,賦值等)都必須在工做內存中進行,而不能直接讀寫主內存中的變量)。不一樣的線程之間沒法直接訪問對方工做內存之間的變量,線程間變量值的傳遞須要經過主內存來完成。git
p.s: 對於上面提到的副本拷貝,好比假設線程中訪問一個10MB的對象,並不會把這10MB的內存複製一份拷貝出來,實際上這個對象的引用,對象中某個在線程訪問到的字段是有可能存在拷貝的,但不會有虛擬機實現把整個對象拷貝一次。github
在併發編程中,咱們一般會遇到如下三個問題:原子性,可見性,有序性,下面咱們咱們來具體看一下這三個特性與volatile
之間的聯繫:編程
public class TestCase { public static int number; public static boolean isinited; public static void main(String[] args) { new Thread( () -> { while (!isinited) { Thread.yield(); } System.out.println(number); } ).start(); number = 20; isinited = true; } }
對於上面的代碼咱們上面的本意是想輸出20
,可是若是運行的話能夠發現輸出的值可能會是0
。這是由於有時候爲了提供程序的效率,JVM會作進行及時編譯,也就是可能會對指令進行重排序,將isInited = true;
放在number = 20;
以前執行,在單線程下面這樣作沒有任何問題,可是在多線程下則會出現重排序問題。若是咱們將number
聲名爲volatile
就能夠很好的解決這個問題,這能夠禁止JVM進行指令重排序,也就意味着number = 20;
必定會在isInited = true
前面執行。緩存
好比對於變量a
,當線程一要修改變量a的值,首先須要將a的值從主存複製過來,再將a的值加一,再將a的值複製回主存。在單線程下面,這樣的操做沒有任何的問題,可是在多線程下面,好比還有一個線程二,在線程一修改a的值的時候,也從主存將a的值複製過來進行加一,隨後線程一和線程二前後將a的值複製回主存,可是主存中a的值最終將只會加一而不是加二。多線程
使用volatile
能夠解決這個問題,它能夠保證在線程一修改a的值以後當即將修改值同步到主存中,這樣線程二拿到的a的值就是線程一已經修改過的a的值了。對volatile變量執行寫操做時,會在寫操做後加入一條store
屏障指令,對volatile變量執行讀操做時,會在寫操做後加入一條load
屏障指令。併發
線程寫volatile變量過程:函數
改變線程工做內存中volatile變量副本的值;atom
將改變後的副本的值從工做內存刷新到主內存。spa
線程讀volatile變量過程:
從主內存中讀取volatile變量的最新值到工做內存中;
從工做內存中讀取volatile變量副本。
原子性是指CPU在執行一條語句的時候,不會中途轉去執行另外的語句。好比i = 1
就是一個原子操做,可是++i
就不是一個原子操做了,由於它要求首先讀取i
的值,而後修改i
的值,最後將值寫入主存中。
可是volatile
卻不能保證程序的原子性,下面咱們經過一個實例來驗證一下:
public class TestCase { public volatile int v = 0; public static final int threadCount = 20; public void increase() { v++; } public static void main(String[] args) { TestCase testCase = new TestCase(); for (int i=0; i<threadCount; i++) { new Thread( () -> { for (int j=0; j<1000; j++) { testCase.increase(); } } ).start(); } while (Thread.activeCount() > 1) { Thread.yield(); } System.out.println(testCase.v); } }
輸出結果:
18921
上面咱們的本意是想讓輸出20000
,可是運行程序後,結果可能會小於20000
。由於v++
它自己並非一個原子操做,它是分爲多個步驟的,並且volatile
自己也並不能保證原子性。
上面的程序使用synchronzied
則能夠很好的解決,只須要聲明public synchronized void increase()
就好了。
或者使用lock也行:
Lock lock = new ReentrantLock(); public void increase() { lock.lock(); try { v++; } finally { lock.unlock(); } }
或者將v
聲明爲AtomicInteger v = new AtomicInteger();
。在java 1.5的java.util.concurrent.atomic包下提供了一些原子操做類,即對基本數據類型的自增,自減,以及加法操做,減法操做進行了封裝,保證這些操做是原子性操做。
下面咱們經過單例模式來看一下volatile
的一個具體應用:
class Singleton { private volatile static Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; } public static void main(String[] args) { Singleton.getInstance(); } }
上面instance
必需要用volatile
修飾,由於new Singleton
是分爲三個步驟的:
給instance指向的對象分配內存,並設置初始值爲null(根據JVM類加載機制的原理,對於靜態變量這一步應該在new Singleton
以前就已經完成了)。
執行構造函數真正初始化instance
將instance指向對象分配內存空間(分配內存空間以後instance就是非null了)
在咱們的步驟2, 3之間的順序是能夠顛倒的,若是線程一在執行步驟3以後並無執行步驟2,可是被線程二搶佔了,線程二獲得的instance
是非null,可是instance卻尚未初始化。而使用volatile則能夠保證程序的有序性。
UNDERSTANDING THE JVM
JAVA CONCURRENCY IN PRACTICE
GitHub: https://github.com/ziwenxie
Blog: https://www.ziwenxie.site
本文爲做者原創,轉載請聲明博客出處:)