Java多線程學習(三)volatile關鍵字

系列文章傳送門:html

Java多線程學習(一)Java多線程入門java

Java多線程學習(二)synchronized關鍵字(1)程序員

java多線程學習(二)synchronized關鍵字(2) 面試

Java多線程學習(三)volatile關鍵字編程

Java多線程學習(四)等待/通知(wait/notify)機制微信

Java多線程學習(五)線程間通訊知識點補充多線程

Java多線程學習(六)Lock鎖的使用併發

Java多線程學習(七)併發編程中一些問題編程語言

系列文章將被優先更新於微信公衆號<font color="red">「Java面試通關手冊」</font>,歡迎廣大Java程序員和愛好技術的人員關注。ide

本節思惟導圖:

volatile關鍵字

思惟導圖源文件+思惟導圖軟件關注微信公衆號:「Java面試通關手冊」回覆關鍵字:「Java多線程」免費領取。

一 簡介

先來看看維基百科對「volatile關鍵字」的定義:

在程序設計中,尤爲是在C語言、C++、C#和Java語言中,使用volatile關鍵字聲明的變量或對象一般具備與優化、多線程相關的特殊屬性。一般,volatile關鍵字用來阻止(僞)編譯器認爲的沒法「被代碼自己」改變的代碼(變量/對象)進行優化。如在C語言中,<font color="red">volatile關鍵字能夠用來提醒編譯器它後面所定義的變量隨時有可能改變,所以編譯後的程序每次須要存儲或讀取這個變量的時候,都會直接從變量地址中讀取數。若是沒有volatile關鍵字,則編譯器可能優化讀取和存儲,可能暫時使用寄存器中的值,若是這個變量由別的程序更新了的話,將出現不一致的現象。據</font>

在C環境中,volatile關鍵字的真實定義和適用範圍常常被誤解。雖然C++、C#和Java都保留了C中的volatile關鍵字,但在這些編程語言中volatile的用法和語義卻截然不同。

Java中的「volatile關鍵字」關鍵字:

在 JDK1.2 以前,Java的內存模型實現老是從<font color="red">主存(即共享內存)讀取變量</font>,是不須要進行特別的注意的。而在當前的 Java 內存模型下,線程能夠把變量保存<font color="red">本地內存</font>(好比機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能形成一個線程在主存中修改了一個變量的值,而另一個線程還繼續使用它在寄存器中的變量值的拷貝,形成<font color="red">數據的不一致</font>。
數據的不一致
要解決這個問題,就須要把變量聲明爲<font color="red"> volatile</font>,這就指示 JVM,這個變量是不穩定的,每次使用它都到主存中進行讀取。
volatile關鍵字的可見性

二 volatile關鍵字的可見性

volatile 修飾的成員變量在每次被線程訪問時,都強迫從主存(共享內存)中重讀該成員變量的值。並且,當成員變量發生變化時,強迫線程將變化值回寫到主存(共享內存)。這樣在任什麼時候刻,兩個不一樣的線程老是看到某個成員變量的同一個值,這樣也就保證了同步數據的可見性

<font size="2">RunThread.java</font>

private boolean isRunning = true;
 int m;
    public boolean isRunning() {
        return isRunning;
    }
    public void setRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }
    @Override
    public void run() {
        System.out.println("進入run了");
        while (isRunning == true) {
            int a=2;
            int b=3;
            int c=a+b;
            m=c;
        }
        System.out.println(m);
        System.out.println("線程被中止了!");
    }
}

<font size="2">Run.java</font>

public class Run {
    public static void main(String[] args) throws InterruptedException {
        RunThread thread = new RunThread();
        
        thread.start();
        Thread.sleep(1000);
        thread.setRunning(false);

        System.out.println("已經賦值爲false");
    }
}

<font size="2">運行結果:</font>
死循環

RunThread類中的isRunning變量沒有加上<font color="red">volatile關鍵字</font>時,運行以上代碼會出現<font color="red">死循環</font>,這是由於isRunning變量雖然被修改可是沒有被寫到<font color="red">主存</font>中,這也就致使該線程在<font color="red">本地內存</font>中的值一直爲true,這樣就致使了死循環的產生。

解決辦法也很簡單:isRunning變量前加上<font color="red">volatile關鍵字</font>便可。
isRunning變量前加上volatile關鍵字
這樣運行就不會出現死循環了。
<font size="2">加上volatile關鍵字後的運行結果:</font>
加上volatile關鍵字後的運行結果

<font color="red">你是否是覺得到這就完了?</font>

不存在的!!!(這裏還有一點須要強調,下面的內容必定要看,否則你在用volatile關鍵字時會很迷糊,由於書籍幾乎都沒有提這個問題)

假如你把while循環代碼里加上任意一個輸出語句或者sleep方法你會發現死循環也會中止,無論isRunning變量是否被加上了上volatile關鍵字。

<font size="2">加上輸出語句:</font>

while (isRunning == true) {
            int a=2;
            int b=3;
            int c=a+b;
            m=c;
            System.out.println(m);
        }

<font size="2">加上sleep方法:</font>

while (isRunning == true) {
            int a=2;
            int b=3;
            int c=a+b;
            m=c;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

<font color="red">這是爲何呢?</font>

<font color="red">由於:JVM會盡力保證內存的可見性,即使這個變量沒有加同步關鍵字</font>。換句話說,只要CPU有時間,JVM會盡力去保證變量值的更新。這種與volatile關鍵字的不一樣在於,volatile關鍵字會強制的保證線程的可見性。而不加這個關鍵字,JVM也會盡力去保證可見性,可是若是CPU一直有其餘的事情在處理,它也沒辦法。最開始的代碼,一直處於死循環中,CPU處於一直佔用的狀態,這個時候CPU沒有時間,JVM也不能強制要求CPU分點時間去取最新的變量值。而<font color="red">加了輸出或者sleep語句以後,CPU就有可能有時間去保證內存的可見性,因而while循環能夠被終止</font>。

三 volatile關鍵字能保證原子性嗎?

《Java併發編程藝術》這本書上說保證可是在自增操做(非原子操做)上不保證,《Java多線程編程核心藝術》這本書說不保證。

我我的更傾向於這種說法:<font color="red">volatile沒法保證對變量原子性的</font>。我我的感受《Java併發編程藝術》這本書上說volatile關鍵字保證原子性嗎可是在自增操做(非原子操做)上不保證這種說法是有問題的。只是我的見解,但願不要被噴。能夠看下面測試代碼:

<font size="2">MyThread.java</font>

public class MyThread extends Thread {
    volatile public static int count;

    private static void addCount() {
        for (int i = 0; i < 100; i++) {
            count=i;
        }
        System.out.println("count=" + count);

    }
    @Override
    public void run() {
        addCount();
    }
}

<font size="2">Run.java</font>

public class Run {
    public static void main(String[] args) {
        MyThread[] mythreadArray = new MyThread[100];
        for (int i = 0; i < 100; i++) {
            mythreadArray[i] = new MyThread();
        }

        for (int i = 0; i < 100; i++) {
            mythreadArray[i].start();
        }
    }

}

<font size="2">運行結果:</font>

上面的「count=i;」是一個原子操做,可是運行結果大部分都是正確結果99,可是也有部分不是99的結果。
運行結果
<font color="red">解決辦法:</font>

<font color="red">使用synchronized關鍵字加鎖</font>。(這只是一種方法,Lock和AtomicInteger原子類均可以,由於以前學過synchronized關鍵字,因此咱們使用synchronized關鍵字的方法)

修改MyThread.java以下:

public class MyThread extends Thread {
    public static int count;

    synchronized private static void addCount() {
        for (int i = 0; i < 100; i++) {
            count=i;
        }
        System.out.println("count=" + count);
    }
    @Override
    public void run() {
        addCount();
    }
}

這樣運行輸出的count就都爲99了,因此<font color="red">要保證數據的原子性仍是要使用synchronized關鍵字</font>。

四 synchronized關鍵字和volatile關鍵字比較

  • volatile關鍵字是線程同步的輕量級實現,因此volatile性能確定比synchronized關鍵字要好。可是volatile關鍵字只能用於變量而synchronized關鍵字能夠修飾方法以及代碼塊。synchronized關鍵字在JavaSE1.6以後進行了主要包括爲了減小得到鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖以及其它各類優化以後執行效率有了顯著提高,實際開發中使用synchronized關鍵字仍是更多一些
  • 多線程訪問volatile關鍵字不會發生阻塞,而synchronized關鍵字可能會發生阻塞
  • volatile關鍵字能保證數據的可見性,但不能保證數據的原子性。synchronized關鍵字二者都能保證。
  • volatile關鍵字用於解決變量在多個線程之間的可見性,而ynchronized關鍵字解決的是多個線程之間訪問資源的同步性。

參考:
《Java多線程編程核心技術》

《Java併發編程的藝術》

極客學院Java併發編程wiki: http://wiki.jikexueyuan.com/p...

若是你以爲博主的文章不錯,歡迎轉發點贊。你能從中學到知識就是我最大的幸運。

歡迎關注個人微信公衆號:「Java面試通關手冊」(分享各類Java學習資源,面試題,以及企業級Java實戰項目回覆關鍵字免費領取)。另外我建立了一個Java學習交流羣(羣號:174594747),歡迎你們加入一塊兒學習,這裏更有面試,學習視頻等資源的分享。

相關文章
相關標籤/搜索