【第十篇】深入學(xué)習(xí)Java虛擬機(jī)之Volatile關(guān)鍵字詳解

1. 概述

關(guān)鍵字volatile可以說時Java虛擬機(jī)提供的最輕量級的同步機(jī)制,但是它并不容易完全被正確、完整的理解,以至于許多程序員都不習(xí)慣去使用它,遇到需要處理并發(fā)問題的時候,一律使用synchronized(synchronized通常稱為重量級鎖)來進(jìn)行同步。

2. 基本概念

Java內(nèi)存模型是圍繞著在并發(fā)過程中如何處理 原子性、可見性有序性 這3個特征來建立的,我們先來看一下這三個特性。

1. 原子性:

由Java內(nèi)存模型來直接保證的原子性變量操作包括read、load、assign、use、store和write,我們大致可以認(rèn)為基本數(shù)據(jù)類型的訪問讀寫是具備原子性的(例外的是long和double的非原子性協(xié)定,讀者只要知道這件事情就可以了)

如果應(yīng)用場景需要一個更大范圍的原子性保證(比如實現(xiàn)一個單例模式的連接等,在實際應(yīng)用中很常見),Java內(nèi)存模型還提供了lock和unlock操作來滿足這種需求,盡管虛擬機(jī)未把lock和unlock操作直接開放給用戶使用,但是卻提供了更高層次的字節(jié)碼指令monitorenter和monitorexit來隱式的使用這兩個操作,這兩個字節(jié)碼指令反映到Java代碼中就是由synchronized關(guān)鍵字標(biāo)識的代碼同步塊,因此在synchronized塊之間的操作也具備原子性。

輔助理解:

int a = 0;

如上是一個簡單的賦值操作(注意:a是非long和double類型的基本類型數(shù)據(jù),double和long類型比較特殊), 這個操作是不可分割的,即,這個操作沒辦法再進(jìn)行拆分,那么我們說這個操作是原子操作。

再比如下面這個操作:

a++;

這個操作實際是a = a + 1;是可分割的,所以這不是一個原子操作。

非原子操作都會存在線程安全問題,需要我們使用同步技術(shù)(如使用sychronized關(guān)鍵字)來讓它變成一個原子操作。

一個操作是原子操作,那么我們稱它具有原子性。

Java的concurrent包下提供了一些原子類,我們可以通過閱讀API來了解這些原子類的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。

2. 可見性:

可見性,是指線程之間的可見性,當(dāng)一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。也就是一個線程修改的結(jié)果,另一個線程馬上就能看到。

Java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方式來實現(xiàn)可見性的。

所以,無論是普通變量還是volatile修飾的變量都是如此,而二者之間的區(qū)別是:volatile的特殊規(guī)則保證了新值能 立即 同步到主內(nèi)存,以及每次使用前 立即 從主內(nèi)存刷新。而普通變量則不能保證這一點(diǎn)。

在 Java 中 volatile、synchronized 和 final 三個關(guān)鍵字可以實現(xiàn)可見性。

可見性是一種復(fù)雜的屬性,因為可見性中的錯誤總是會違背我們的直覺。通常,我們無法確保執(zhí)行讀操作的線程能適時地看到其他線程寫入的值,有時甚至是根本不可能的事情。為了確保多個線程之間對內(nèi)存寫入操作的可見性,必須使用同步機(jī)制。

比如,用volatile修飾的變量會具有可見性。volatile修飾的變量不允許線程內(nèi)部緩存和重排序,即,直接修改內(nèi)存,所以對其他線程是可見的。

但是這里需要注意一個問題:volatile只能讓被它修飾的變量具有可見性,但不能保證它具有原子性。

如下操作:

volatile int a = 0;
……其他操作
a++;
……其他操作

這個變量a具有可見性,但是a++依然是一個非原子操作,也就是這個操作同樣存在線程安全問題。

3. 有序性:

Java程序中天然的有序性可以總結(jié)為一句話:如果在本線程內(nèi)觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。

前半句是指線程內(nèi)表現(xiàn)為串行的語義,后半句是指“指令重排序”現(xiàn)象和“工作內(nèi)存與主內(nèi)存同步延遲”現(xiàn)象

Java 語言提供了 volatile 和 synchronized 兩個關(guān)鍵字來保證線程之間操作的有序性,volatile 是因為其本身包含“禁止指令重排序”的語義,synchronized 是由“一個變量在同一個時刻只允許一條線程對其進(jìn)行 lock 操作”這條規(guī)則獲得的,此規(guī)則決定了持有同一個對象鎖的兩個同步塊只能串行執(zhí)行。

3. volatile原理

Java語言提供了一種稍弱的同步機(jī)制,即volatile變量,用來確保將變量的更新操作通知到其他線程。

當(dāng)把變量聲明為volatile類型后,編譯器與運(yùn)行時都會注意到這個變量是共享的,因此不會將該變量上的操作與其他內(nèi)存操作一起重排序。

volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。

在訪問volatile變量時不會執(zhí)行加鎖操作,因此也就不會使執(zhí)行線程阻塞,因此volatile變量是一種比sychronized關(guān)鍵字更輕量級的同步機(jī)制。

當(dāng)對非 volatile 變量進(jìn)行讀寫的時候,每個線程先從內(nèi)存拷貝變量到CPU緩存中。如果計算機(jī)有多個CPU,每個線程可能在不同的CPU上被處理,這意味著每個線程可以拷貝到不同的 CPU cache 中。

而聲明變量是 volatile 的,JVM 保證了每次讀變量都從內(nèi)存中讀,跳過 CPU cache 這一步。

當(dāng)一個變量定義為 volatile 之后,將具備兩種特性:

  1. 保證此變量對所有的線程的可見性,這里的“可見性”是指:當(dāng)一個線程修改了這個變量的值,volatile 保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新。但普通變量做不到這點(diǎn),普通變量的值在線程間傳遞均需要通過主內(nèi)存來完成。

注意:
在講原子性的時候講到過,volatile修飾的變量并不能保證原子性操作,即,如果被volatile修飾的變量的運(yùn)算并非原子操作,那么在并發(fā)的情況下,該運(yùn)算同樣是不安全的。也即,volatile修飾的變量只能保證可見性和有序性,不一定能保證原子性。

如下代碼,就會出現(xiàn)并發(fā)問題:

package JavaCore;

public class VolatileTest {
    public static volatile int race = 0;

    public static void increase() {
        //  可拆解為race = race + 1;,而race + 1也不是原子性的操作
        race++;
    }

    private static final int THREADS_COUNT = 20;

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }

        // 等待1分鐘,足夠運(yùn)算完成了
        try {
            Thread.sleep(60000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(race);
    }
}

我們期待的結(jié)果是20000,但最終的結(jié)果往往比這個結(jié)果要小。

所以,在不滿足如下兩條規(guī)則的運(yùn)算場景中,我們?nèi)匀灰ㄟ^加鎖(使用synchronized關(guān)鍵字或使用java.util.concurrent中的原子類)來保證原子性。規(guī)則如下:
a. 運(yùn)算結(jié)果并不依賴變量的當(dāng)前值,或者能夠確保只有單一的線程修改變量的值。

b. 變量不需要與其他的狀態(tài)變量共同參與不變約束。

結(jié)合上面的代碼看,我們可以看出在increase()方法中,race的結(jié)果是依賴當(dāng)前race值的。

下面代碼展示的就是很適合使用volatile變量來控制并發(fā),當(dāng)shutdown()方法被調(diào)用時,能保證所有線程中執(zhí)行的doWork()方法立即停下來。

volatile boolean shutdownRequested;
public void shutdown() {
  shutdownRequested = true;
}

public void doWork() {
  while(!shutdownRequested) {
    // do stuff
  }
}
  1. 禁止指令重排序優(yōu)化。有volatile修飾的變量,賦值后多執(zhí)行了一個“l(fā)oad addl $0x0, (%esp)”操作,這個操作相當(dāng)于一個內(nèi)存屏障(指令重排序時不能把后面的指令重排序到內(nèi)存屏障之前的位置),只有一個CPU訪問內(nèi)存時,并不需要內(nèi)存屏障;

什么是指令重排序:是指CPU采用了允許將多條指令不按程序規(guī)定的順序分開發(fā)送給各相應(yīng)電路單元處理。

volatile 性能:

volatile 的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因為它需要在本地代碼中插入許多內(nèi)存屏障指令來保證處理器不發(fā)生亂序執(zhí)行。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容