volatile關鍵字

要想更好的理解volatile關鍵字,咱們先來聊聊基於高速緩存的存儲交互緩存

image.png

咱們知道程序中進行計算的變量是存儲在內存中的,而處理器的計算速度和內存的讀取速度徹底不在一個量級,區別猶如蘭博基尼和自行車。安全

要讓蘭博基尼開一小段就停下來等會自行車顯然不太合適,因此在處理器和內存之間加了一個高速緩存,高速緩存速度遠高於內存,猶如奔馳,雖然和蘭博基尼還有必定差距,每一個處理器都對應一個高速緩存。多線程

當要對一個變量進行計算的時候,先從內存中將該變量的值讀取到高速緩存中,再去計算,效率獲得明顯提高,這是從硬件的的視角描述的內存ide

Jvm虛擬機從另外一個視角定義的內存模型規定全部變量都存儲在主內存中,每一個線程有本身的工做內存,每一個線程的工做內存只能被該線程獨佔,其它線程不能訪問,全部的線程只能經過主內存來共享數據性能

這裏的主內存能夠類比於硬件視角的內存,工做內存能夠類比於硬件視角的高速緩存優化

線程執行程序的時候先將主內存中的變量複製到工做內存中進行計算,計算完畢後再將變量同步到主內存中。spa

這麼作雖然解決了執行效率的問題,可是同時也帶來了其它問題。線程

試想一下,線程A從主內存中複製了一個變量a=3到工做內存,而且對變量a進行了加一操做,a變成了4,此時線程B也從主內存中複製該變量到它本身的工做內存,它獲得的a的值仍是3,a的值不一致了。排序

用專業術語來講就是變量的可見性,此時變量a對於線程來講變得不可見了內存

怎麼解決這個問題?

volatile關鍵字閃亮登場:

當一個變量被定義爲volatile以後,它對全部的線程就具備了可見性,也就是說當一個線程修改了該變量的值,全部的其它線程均可以當即知道,能夠從兩個方面來理解這句話:

1.線程對變量進行修改以後,要馬上回寫到主內存。

2.線程對變量讀取的時候,要從主內存中讀,而不是工做內存。

可是這並不意味着使用了volatile關鍵字的變量具備了線程安全性,舉個栗子:

public class AddThread implements Runnable {

    private volatile int num=0;

    @Override

    public void run() {

        for (int i=1;i<=10000;i++){

            num=num+1;

            System.out.println(num);

        }

    }

}

 

 

 

public class VolatileThread {

    public static void main(String[] args) {

        Thread[] th = new Thread[20];

        AddThread addTh = new AddThread();

        for(int i=1;i<=20;i++){

            th[i] = new Thread(addTh);

            th[i].start();

        }

    }

}

 

這裏咱們建立了20個線程,每一個線程對num進行10000次累加。

按理結果應該是打印1,2,3.。。。。。200000 。

可是結果倒是1,2,3…..x ,x小於200000.

爲何會是這樣的結果?

咱們仔細分析一下這行代碼:num=num+1;

雖然只有一行代碼,可是被編譯爲字節碼之後會對應四條指令:

1.Getstatic將num的值從主內存取出到線程的工做內存

2.Iconst_1 和 iadd 將num的值加一

3.Putstatic將結果同步回主內存

在第一步Getstatic將num的值從主內存取出到線程的工做內存由於num加了Volatile關鍵字,能夠保證它的值是正確的,可是在執行第二步的時候其它的線程有可能已經將num的值加大了。在第三步就會將較小的值同步到內存,因而形成了咱們看到的結果。

既然如此,Volatile在什麼場合下能夠用到呢?

一個變量,若是有多個線程只有一個線程會去修改這個變量,其它線程都只是讀取該變量的值就可使用Volatile關鍵字,爲何呢?一個線程修改了該變量,其它線程會馬上獲取到修改後的值。

由於Volatile的特性能夠保證這些線程獲取到的都是正確的值,而他們又不會去修改這個變量,不會形成該變量在各個線程中不一致的狀況。固然這種場合也能夠用synchronized關鍵字

當運算結果並不依賴變量的當前值的時候該變量也可使用Volatile關鍵字,上栗子:

public class shutDownThread implements Runnable {

    volatile boolean shutDownRequested;

    public void shutDown(){

        shutDownRequested = true;

    }

    @Override

    public void run() {

        while (!shutDownRequested) {

            System.out.println("work!");

        }

    }

}

 

 

public class Demo01 {

    public static void main(String[] args) throws InterruptedException {

        Thread[] th = new Thread[10];

        shutDownThread t = new shutDownThread();

        for(int i=0;i<=9;i++){

            th[i] = new Thread(t);

            th[i].start();

        }

        Thread.sleep(2000);

        t.shutDown();

    }

}

 

當調用t.shutDown()方法將shutDownRequested的值設置爲true之後,由於shutDownRequested 使用了volatile ,全部線程都獲取了它的最新值true,while循環的條件「!shutDownRequested」再也不成立,「 System.out.println("work!");」打印work的代碼也就中止了執行。

Volatile還能夠用來禁止指令重排序

什麼是指令重排序?

Int num1 = 3;          1

Int num2 = 4;          2

Int num3 = num1+num2;  3

在這段代碼中cpu在執行的時候會對代碼進行優化,以達到更快的執行速度,有可能會交換1處和2處的代碼執行的順序,這就是指令重排序。

指令重排序並非爲了執行速度不擇手段的任意重排代碼順序,這樣必然會亂套,重排序必須遵循必定的規則,1處和2處的代碼之間沒有任何關係,他們的執行順序對結果不會照成任何影響,也就是說1->2>3的執行和2->1->3的執行最後結果都爲num3=7.咱們說1處和2處的操做沒有數據依賴性,沒有數據依賴性的代碼能夠重排序。

再看一下2處和3處的代碼,若是把他們交換順序,結果會不同,爲何會不同呢?由於這兩處操做都操做了num2這個變量,而且在第二處操做中修改了num2的值。

若是有兩個操做操做了同一個變量,而且其中一個爲寫操做,那麼這兩個操做就存在數據依賴性,對於有數據依賴性的操做,不能重排序,因此2處和3處的操做不能重排序。

還有一個規則是不管怎麼從新排序,單線程的執行結果不能被改變,也就是說在單線程的狀況下,咱們是感覺不到重排序帶來的影響的

在多線程的狀況下重排序會對程序形成什麼影響呢?

舉個栗子:

//定義一個布爾型的變量表示是否讀取配置文件,初始爲未讀取

Volatile boolean flag = false; 1

 

//線程A執行 讀取配置文件之後將flag改成true

readConfig(); 2

flag = true; 3

 

//線程B執行循環檢測flag,若是爲false表示未讀取配置文件,則休眠。若是爲true表示已讀取配置文件,則執行doSomething()

while(!flag){ 4

sleep(); 5

}

doSomething(); 6

在這段僞代碼中若是1處的代碼沒有用Volatile關鍵字,可能因爲指令重排序的優化,在A線程中,3處的代碼 flag=true在2處代碼以前執行,致使B線程在配置文件還未讀取的狀況下去執行相關操做,從而引發錯誤。

Volatile關鍵字能夠避免這種狀況發生。

他是如何作到的呢?

經過彙編代碼能夠看出,在3處當咱們對Volatile修飾的變量作賦值操做的時候,多執行了一個指令 「lock add1 $0x0,(%esp)」.

這個指令的做用是使該指令以後的全部操做不能重排序到該指令的前面,專業術語叫作內存屏障。

正是由於內存屏障的存在可以保證代碼的正確執行,因此讀取Volatile關鍵字修飾的變量和普通變量沒有什麼差異,可是作寫入操做的時候因爲要插入內存屏障,會影響到效率。

實際上在jdk對Synchronized進行優化之後,Synchronized的性能明顯提高和Volatile已經差異不大了,Volatile的用法比較複雜,容易出錯,Synchronized也能夠解決變量可見性的問題,因此一般狀況下咱們優先選擇Synchronized,可是Synchronized不能禁止指令重排序,貌似這是Volatile的適用場合。

相關文章
相關標籤/搜索