1. 程式人生 > 其它 >volatile可見性的理解

volatile可見性的理解

在多執行緒併發程式設計中synchronized和Volatile都扮演著重要的角色,Volatile是****輕量級的synchronized,它在多處理器開發中保證了共享變數的“可見性”。可見性的意思是當一個執行緒修改一個共享變數時,另外一個執行緒能讀到這個修改的值。它在某些情況下比synchronized的開銷更小。

Volatile的官方定義

Java語言規範第三版中對volatile的定義如下: java程式語言允許執行緒訪問共享變數,為了確保共享變數能被準確和一致的更新,執行緒應該確保通過排他鎖單獨獲得這個變數。Java語言提供了volatile,在某些情況下比鎖更加方便。如果一個欄位被宣告成volatile,java執行緒記憶體模型確保所有執行緒看到這個變數的值是一致的。

為什麼要使用Volatile

Volatile變數修飾符如果使用恰當的話,它比synchronized的使用和執行成本會更低,因為它不會引起執行緒上下文的切換和排程。

記憶體可見性

由於 Java 記憶體模型( JMM)規定,所有的變數都存放在主記憶體中,而每個執行緒都有著自己的工作記憶體(快取記憶體)。

執行緒在工作時,需要將主記憶體中的資料拷貝到工作記憶體中。這樣對資料的任何操作都是基於工作記憶體(效率提高),並且不能直接操作主記憶體以及其他執行緒工作記憶體中的資料,之後再將更新之後的資料重新整理到主記憶體中。

這裡所提到的主記憶體可以簡單認為是堆記憶體,而工作記憶體則可以認為是棧記憶體

如下圖所示:

所以在併發執行時可能會出現執行緒 B 所讀取到的資料是執行緒 A 更新之前的資料。

顯然這肯定是會出問題的,因此 volatile 的作用出現了:

當一個變數被 volatile 修飾時,任何執行緒對它的寫操作都會立即重新整理到主記憶體中,並且會強制讓快取了該變數的執行緒中的資料清空,必須從主記憶體重新讀取最新資料。

volatile 修飾之後並不是讓執行緒直接從主記憶體中獲取資料,依然需要將變數拷貝到工作記憶體中。

本文主要針對 共享變數 可見性理解的演示。

程式碼片段1

public class T01_HelloVolatile {
    /*volatile*/ boolean running = true; //對比一下有無volatile的情況下,整個程式執行結果的區別

    void m() {
        System.out.println("m  修改【running】值前進行啟動 ,【running】的值為" + running);
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        while (running) {

        }
        System.out.println("m end!");
    }

    void n() {
        System.out.println("n 進行【running】的修改,修改前 【running】值為" + running);
        running = false;
        System.out.println("n 進行【running】的修改,修改後 【running】值為" + running);
    }

    void f() {
        System.out.println("f  修改【running】值前進行啟動 ,【running】的值為" + running);

        while (running) {
        }
        System.out.println("f end ,【running】的值為" + running);
    }

    public static void main(String[] args) throws InterruptedException {
        T01_HelloVolatile t = new T01_HelloVolatile();

        new Thread(t::m, "t1").start();

        TimeUnit.SECONDS.sleep(5);
        new Thread(t::n, "t2").start();

        TimeUnit.SECONDS.sleep(1);
        new Thread(t::f, "t3").start();
    }

}
```
此段程式碼演示了 volatile .
- 先啟動執行緒 m,m一直持有 running變數。
- 在啟動執行緒n,n執行緒進行running屬性的更改。
- 在啟動執行緒f ,在f執行緒裡面讀取 running變數,驗證是否可以拿到running修改後的變數。

以上程式碼輸出結果:
 
m  修改【running】值前進行啟動 ,【running】的值為true
n 進行【running】的修改,修改前 【running】值為true
n 進行【running】的修改,修改後 【running】值為false
f  修改【running】值前進行啟動 ,【running】的值為false
f end ,【running】的值為false  

結果顯示 m 執行緒未結束,f執行緒結束
 
以上說明, 執行緒n修改了running值之後,之後的f執行緒來進行訪問這個值的時候是能訪問到修改的內容的。線上程n修改這個值之前啟動的執行緒m是沒法訪問到執行緒n修改的這個值的內容的。

程式碼片段2

public class T01_HelloVolatile {
    /*volatile*/ boolean running = true; //對比一下有無volatile的情況下,整個程式執行結果的區別

    void m() {
        System.out.println("m  修改【running】值前進行啟動 ,【running】的值為" + running);
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        while (running) {
            // 此處千萬不要 使用 System.out.println 此句程式碼會觸發同步機制

        }
        System.out.println("m end!");
    }

    void n() {
        System.out.println("n 進行【running】的修改,修改前 【running】值為" + running);
        running = false;
        System.out.println("n 進行【running】的修改,修改後 【running】值為" + running);
    }

    void f() {
        System.out.println("f  修改【running】值前進行啟動 ,【running】的值為" + running);

        while (running) {
        }
        System.out.println("f end ,【running】的值為" + running);
    }

    public static void main(String[] args) throws InterruptedException {
        T01_HelloVolatile t = new T01_HelloVolatile();

        new Thread(t::m, "t1").start();

        TimeUnit.SECONDS.sleep(1);
        new Thread(t::n, "t2").start();

        TimeUnit.SECONDS.sleep(1);
        new Thread(t::f, "t3").start();
    }

}
 

此段程式碼比 程式碼片段1 中 m執行緒多了一段睡眠時間,加上 5 秒的睡眠時間之後,執行結果如下:
 
m  修改【running】值前進行啟動 ,【running】的值為true
n 進行【running】的修改,修改前 【running】值為true
n 進行【running】的修改,修改後 【running】值為false
f  修改【running】值前進行啟動 ,【running】的值為false
f end ,【running】的值為false
m end!

m 執行緒正常結束。 那麼 片段1 中的總結 就不正確,應該正確理解為 一直持有的這個變數 通過volatile 修飾後,當其他執行緒修改這個變數之後,這個變數在當前執行緒就可見

程式碼片段3

public class T01_HelloVolatile {
    volatile boolean running = true; //對比一下有無volatile的情況下,整個程式執行結果的區別

    void m() {
        System.out.println("m  修改【running】值前進行啟動 ,【running】的值為" + running);
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        while (running) {
            // 此處千萬不要 使用 System.out.println 此句程式碼會觸發同步機制

        }
        System.out.println("m end!");
    }

    void n() {
        System.out.println("n 進行【running】的修改,修改前 【running】值為" + running);
        running = false;
        System.out.println("n 進行【running】的修改,修改後 【running】值為" + running);
    }

    void f() {
        System.out.println("f  修改【running】值前進行啟動 ,【running】的值為" + running);

        while (running) {
        }
        System.out.println("f end ,【running】的值為" + running);
    }

    public static void main(String[] args) throws InterruptedException {
        T01_HelloVolatile t = new T01_HelloVolatile();

        new Thread(t::m, "t1").start();

        TimeUnit.SECONDS.sleep(5);
        new Thread(t::n, "t2").start();

        TimeUnit.SECONDS.sleep(1);
        new Thread(t::f, "t3").start();
    }
}
此段程式碼和程式碼片段1 做比較因為多了 volatile 關鍵字,所以 m f 執行緒都能正常結束,和程式碼片段1 相比
來說明來 volatile 修飾的變數是可見的。