Java併發編程之volatile關鍵字解析

引言

volatile關鍵字雖然從字面上理解起來比較簡單,可是要用好不是一件容易的事情。本文咱們就從JVM內存模型開始,瞭解一下volatile的應用場景。java

JVM內存模型

在瞭解volatile以前,咱們有必要對JVM的內存模型有一個基本的瞭解。Java的內存模型規定了全部的變量都存儲在主內存中(即物理硬件的內存),每條線程還具備本身的工做內存(工做內存可能位於處理器的高速緩存之中),線程的工做內存中保存了該線程使用到的變量的主內存副本拷貝,線程對變量的全部操做(讀取,賦值等)都必須在工做內存中進行,而不能直接讀寫主內存中的變量)。不一樣的線程之間沒法直接訪問對方工做內存之間的變量,線程間變量值的傳遞須要經過主內存來完成。git

JVM內存模型

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變量過程:函數

  1. 改變線程工做內存中volatile變量副本的值;atom

  2. 將改變後的副本的值從工做內存刷新到主內存。spa

線程讀volatile變量過程:

  1. 從主內存中讀取volatile變量的最新值到工做內存中;

  2. 從工做內存中讀取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是分爲三個步驟的:

  1. 給instance指向的對象分配內存,並設置初始值爲null(根據JVM類加載機制的原理,對於靜態變量這一步應該在new Singleton以前就已經完成了)。

  2. 執行構造函數真正初始化instance

  3. 將instance指向對象分配內存空間(分配內存空間以後instance就是非null了)

在咱們的步驟2, 3之間的順序是能夠顛倒的,若是線程一在執行步驟3以後並無執行步驟2,可是被線程二搶佔了,線程二獲得的instance是非null,可是instance卻尚未初始化。而使用volatile則能夠保證程序的有序性。

References

UNDERSTANDING THE JVM
JAVA CONCURRENCY IN PRACTICE

Contact

GitHub: https://github.com/ziwenxie
Blog: https://www.ziwenxie.site

本文爲做者原創,轉載請聲明博客出處:)

相關文章
相關標籤/搜索