JAVA并發(fā)編程(一):理解volatile關(guān)鍵字

volatile_logo

Java中volatile這個(gè)熱門的關(guān)鍵字,在面試中經(jīng)常會(huì)被提及,在各種技術(shù)交流群中也經(jīng)常被討論:volatile關(guān)鍵字在java多線程中有著比較重要作用,volatile主要作用是可以保持變量在多線程中是實(shí)時(shí)可見的,是java中提供的最輕量的同步機(jī)制。

一、JAVA內(nèi)存模型概述

在了解volatile關(guān)鍵字之前,我們先來認(rèn)識一下Java的內(nèi)存模型。
Java線程之間的通信由Java內(nèi)存模型(本文簡稱為JMM)控制,JMM決定一個(gè)線程對共享變量的寫入何時(shí)對另一個(gè)線程可見。從抽象的角度來看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲在主內(nèi)存(Main Memory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(Local Memory),本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本。本地內(nèi)存是JMM的一個(gè)抽象概念,并不真實(shí)存在。它涵蓋了緩存、寫緩沖區(qū)、寄存器以及其他的硬件和編譯器優(yōu)化。Java內(nèi)存模型的抽象示意如圖所示


volatile_1

如果線程A與線程B之間要通信的話,必須要經(jīng)歷下面2個(gè)步驟。
1)線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去。
2)線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量。
這個(gè)模型在單線程中沒有什么問題,但是在多線程中就會(huì)產(chǎn)生一些數(shù)據(jù)的“臟讀”等問題。
舉個(gè)簡單的例子:在java中,使用兩個(gè)線程執(zhí)行下面這個(gè)語句:

int i = 0;
i = i + 1;

我們期望的是在兩個(gè)線程執(zhí)行完之后獲得i的結(jié)果是2,但事實(shí)真的會(huì)這樣的嗎?
我們反復(fù)執(zhí)行后可以發(fā)現(xiàn):結(jié)果可能是2,也可能是1。
每條線程執(zhí)行時(shí)需要將i的值從主內(nèi)存中讀取到工作內(nèi)存中。其中存在這么一種情況:初始時(shí),兩個(gè)線程分別讀取i的值存入各自所在的工作內(nèi)存當(dāng)中,然后線程1進(jìn)行加1操作,然后把i的最新值1寫入到內(nèi)存。此時(shí)線程2的工作內(nèi)存當(dāng)中i的值還是0,進(jìn)行加1操作之后,i的值為1,然后線程2把i的值寫入內(nèi)存。當(dāng)出現(xiàn)這種情況后返回的結(jié)果就成了1了。
這就是緩存一致性的問題,在解決這個(gè)問題前我們要先了解一下并發(fā)編程的三個(gè)概念:原子性,有序性,可見性。

二、并發(fā)編程中的三個(gè)概念

1.原子性

定義:原子操作意 為“不可被中斷的一個(gè)或一系列操作。
比如 a=0;(a非long和double類型) 這個(gè)操作是不可分割的,那么我們說這個(gè)操作是原子操作。再比如:a++; 這個(gè)操作實(shí)際是a = a + 1;是可分割的,所以他不是一個(gè)原子操作。非原子操作都會(huì)存在線程安全問題,需要我們使用同步技術(shù)(sychronized)來讓它變成一個(gè)原子操作。如果一個(gè)操作是原子操作,那么我們稱它具有原子性。java的concurrent包下提供了一些原子類,我們可以通過閱讀API來了解這些原子類的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
如果要實(shí)現(xiàn)更大范圍操作的原子性,可以通過CAS算法來實(shí)現(xiàn)。

2.可見性

定義:可見性是指當(dāng)一個(gè)線程修改一個(gè)共享變量時(shí),另外一個(gè)線程能讀到這個(gè)修改的值。
舉個(gè)簡單的例子,看下面這段代碼:

//線程1執(zhí)行的代碼
int i = 0;
i = 10;
 
//線程2執(zhí)行的代碼
j = i;

由上面的分析可知,當(dāng)線程1執(zhí)行 i =10這句時(shí),會(huì)先把i的初始值加載到工作內(nèi)存中,然后賦值為10,那么在線程1的工作內(nèi)存當(dāng)中i的值變?yōu)?0了,卻沒有立即寫入到主存當(dāng)中。

此時(shí)線程2執(zhí)行 j = i,它會(huì)先去主存讀取i的值并加載到線程2的工作內(nèi)存當(dāng)中,注意此時(shí)內(nèi)存當(dāng)中i的值還是0,那么就會(huì)使得j的值為0,而不是10.

這就是可見性問題,線程1對變量i修改了之后,線程2沒有立即看到線程1修改的值。

3.有序性

定義:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。

int i = 0;              
boolean flag = false;
i = 1;                //語句1  
flag = true;          //語句2

上面代碼定義了一個(gè)int型變量,定義了一個(gè)boolean類型變量,然后分別對兩個(gè)變量進(jìn)行賦值操作。從代碼順序上看,語句1是在語句2前面的,那么JVM在真正執(zhí)行這段代碼的時(shí)候會(huì)保證語句1一定會(huì)在語句2前面執(zhí)行嗎?不一定,為什么呢?這里可能會(huì)發(fā)生指令重排序(Instruction Reorder)。
下面解釋一下什么是指令重排序,一般來說,處理器為了提高程序運(yùn)行效率,可能會(huì)對輸入代碼進(jìn)行優(yōu)化,它不保證程序中各個(gè)語句的執(zhí)行先后順序同代碼中的順序一致,但是它會(huì)保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。
比如上面的代碼中,語句1和語句2誰先執(zhí)行對最終的程序結(jié)果并沒有影響,那么就有可能在執(zhí)行過程中,語句2先執(zhí)行而語句1后執(zhí)行。
但是要注意,雖然處理器會(huì)對指令進(jìn)行重排序,但是它會(huì)保證程序最終結(jié)果會(huì)和代碼順序執(zhí)行結(jié)果相同,那么它靠什么保證的呢?再看下面一個(gè)例子:

int a = 10;    //語句1
int r = 2;    //語句2
a = a + 3;    //語句3
r = a*a;     //語句4

這段代碼有4個(gè)語句,那么可能的一個(gè)執(zhí)行順序是:


volatile_2

不可能,因?yàn)樘幚砥髟谶M(jìn)行重排序時(shí)是會(huì)考慮指令之間的數(shù)據(jù)依賴性,如果一個(gè)指令I(lǐng)nstruction 2必須用到Instruction 1的結(jié)果,那么處理器會(huì)保證Instruction 1會(huì)在Instruction 2之前執(zhí)行。

雖然重排序不會(huì)影響單個(gè)線程內(nèi)程序執(zhí)行的結(jié)果,但是多線程呢?下面看一個(gè)例子:

//線程1:

context = loadContext();   //語句1
inited = true;             //語句2

 //線程2:
while(!inited ){
   sleep()
}
doSomethingwithconfig(context);

上面代碼中,由于語句1和語句2沒有數(shù)據(jù)依賴性,因此可能會(huì)被重排序。假如發(fā)生了重排序,在線程1執(zhí)行過程中先執(zhí)行語句2,而此是線程2會(huì)以為初始化工作已經(jīng)完成,那么就會(huì)跳出while循環(huán),去執(zhí)行doSomethingwithconfig(context)方法,而此時(shí)context并沒有被初始化,就會(huì)導(dǎo)致程序出錯(cuò)。
從上面可以看出,指令重排序不會(huì)影響單個(gè)線程的執(zhí)行,但是會(huì)影響到線程并發(fā)執(zhí)行的正確性。
也就是說,要想并發(fā)程序正確地執(zhí)行,必須要保證原子性、可見性以及有序性。只要有一個(gè)沒有被保證,就有可能會(huì)導(dǎo)致程序運(yùn)行不正確。

三、深入理解volatile關(guān)鍵字

在多線程并發(fā)編程中synchronized和volatile都扮演著重要的角色,volatile是輕量級的 synchronized。如果volatile變量修飾符使用恰當(dāng) 的話,它比synchronized的使用和執(zhí)行成本更低,因?yàn)樗粫?huì)引起線程上下文的切換和調(diào)度。

1.volatile的作用

一旦一個(gè)共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:
  1)保證了內(nèi)存的可見性,即一個(gè)線程修改了某個(gè)變量的值,這新值對其他線程來說是立即可見的。
  2)禁止進(jìn)行指令重排序。

1.volatile不能保證原子性

我們來看下面這一段代碼

/*
 * i++ 的原子性問題:i++ 的操作實(shí)際上分為三個(gè)步驟“讀-改-寫”
 *        int i = 10;
 *        i = i++; //10
 * 
 *        int temp = i;
 *        i = i + 1;
 *        i = temp;
 */
public class TestAtomicDemo {

    public static void main(String[] args) {
        AtomicDemo ad = new AtomicDemo();
        
        for (int i = 0; i < 10; i++) {
            new Thread(ad).start();
        }
    }
    
}

class AtomicDemo implements Runnable{
    
    private volatile int serialNumber = 0;
    
    @Override
    public void run() {
        
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
        }
        
        System.out.println(getSerialNumber());
    }
    
    public int getSerialNumber(){
        return serialNumber++;
    }
}

反復(fù)運(yùn)行這段代碼,發(fā)現(xiàn)結(jié)果并不是每次都是0-9,而是有可能會(huì)有重復(fù)結(jié)果出現(xiàn)。
自增操作是不具備原子性的,它包括讀取變量的原始值、進(jìn)行加1操作、寫入工作內(nèi)存。那么就是說自增操作的三個(gè)子操作可能會(huì)分割開執(zhí)行:
線程1對變量進(jìn)行讀取操作之后,被阻塞了的話,并沒有對serialNumber值進(jìn)行修改。雖然volatile能保證線程2對變量serialNumber的值讀取是從內(nèi)存中讀取的,但是線程1沒有進(jìn)行修改,所以線程2根本就不會(huì)看到修改的值。

根源就在這里,自增操作不是原子性操作,而且volatile也無法保證對變量的任何操作都是原子性的。那么原子性問題究竟應(yīng)該怎么解決呢?我們在下篇文章中會(huì)給出詳細(xì)解答。

四、volatile關(guān)鍵字的應(yīng)用場景

synchronized關(guān)鍵字是防止多個(gè)線程同時(shí)執(zhí)行一段代碼,那么就會(huì)很影響程序執(zhí)行效率,而volatile關(guān)鍵字在某些情況下性能要優(yōu)于synchronized,但是要注意volatile關(guān)鍵字是無法替代synchronized關(guān)鍵字的,因?yàn)関olatile關(guān)鍵字無法保證操作的原子性。通常來說,使用volatile必須具備以下2個(gè)條件:

1)對變量的寫操作不依賴于當(dāng)前值
2)該變量沒有包含在具有其他變量的不變式中

下面列舉幾個(gè)Java中使用volatile的幾個(gè)場景。
1.狀態(tài)標(biāo)記量

volatile boolean flag = false;
 //線程1
while(!flag){
    doSomething();
}
  //線程2
public void setFlag() {
    flag = true;
}

根據(jù)狀態(tài)標(biāo)記,終止線程。

2.單例模式中的double check

class Singleton{
    private volatile static Singleton instance = null;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

為什么要使用volatile 修飾instance?
主要在于instance = new Singleton()這句,這并非是一個(gè)原子操作,事實(shí)上在 JVM 中這句話大概做了下面 3 件事情:
1.給 instance 分配內(nèi)存
2.調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量
3.將instance對象指向分配的內(nèi)存空間(執(zhí)行完這步 instance 就為非 null 了)。

但是在 JVM 的即時(shí)編譯器中存在指令重排序的優(yōu)化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執(zhí)行完畢、2 未執(zhí)行之前,被線程二搶占了,這時(shí) instance 已經(jīng)是非 null 了(但卻沒有初始化),所以線程二會(huì)直接返回 instance,然后使用,然后順理成章地報(bào)錯(cuò)。

參考文章


本文作者: catalinaLi
本文鏈接: http://catalinali.top/2018/helloVolatile/

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

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

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