Java並發編程之Volatile變量詳解分析

Volatile關鍵字是Java提供的一種輕量級的同步機制。Java 語言包含兩種內在的同步機制:同步塊(或方法)和 volatile 變量, 相比synchronized(synchronized通常稱為重量級鎖),volatile更輕量級,因為它不會引起線程上下文的切換和調度。 但是volatile 變量的同步性較差(有時它更簡單並且開銷更低),而且其使用也更容易出錯。

一、volatile變量的特性

1.1、保證可見性,不保證原子性

  • 當寫一個volatile變量時,JMM會把該線程本地內存中的變量強制刷新到主內存中去;
  • 這個寫會操作會導致其他線程中的volatile變量緩存無效。

來看一段代碼:

public class Test {
    public static void main(String[] args) {
        WangZai wangZai = new WangZai();
        wangZai.start();
        for(; ;){
            if(wangZai.isFlag()){
                System.out.println("hello");
            }
        }
    }
 
    static class WangZai extends Thread {
 
        private boolean flag = false;
 
        public boolean isFlag(){
            return flag;
        }
 
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = true;
            System.out.println("flag = " + flag);
        }
    }
}

你會發現,永遠都不會輸出hello這一段代碼,按道理線程改瞭flag變量,主線程也能訪問到的呀?

但是將flag變量用volatile修飾一下,就能輸出hello這段代碼

private volatile boolean flag = false;

每個線程操作數據的時候會把數據從主內存讀取到自己的工作內存,如果他操作瞭數據並且寫會瞭,那其他已經讀取的線程的變量副本就會失效瞭,需要對數據進行操作又要再次去主內存中讀取瞭。

volatile保證不同線程對共享變量操作的可見性,也就是說一個線程修改瞭volatile修飾的變量,當修改寫回主內存時,另外一個線程立即看到最新的值。

1.2、禁止指令重排

重排序需要遵守一定規則:

  • 重排序操作不會對存在數據依賴關系的操作進行重排序。
  • 重排序是為瞭優化性能,但是不管怎麼重排序,單線程下程序的執行結果不能被改變。

什麼是重排序?

為瞭提高性能,編譯器和處理器常常會對既定的代碼執行順序進行指令重排序。

重排序的類型有哪些呢?

深入淺出談談Java並發編程:Volatile

一個好的內存模型實際上會放松對處理器和編譯器規則的束縛,也就是說軟件技術和硬件技術都為同一個目標,而進行奮鬥:在不改變程序執行結果的前提下,盡可能提高執行效率。

JMM對底層盡量減少約束,使其能夠發揮自身優勢。

因此,在執行程序時,為瞭提高性能,編譯器和處理器常常會對指令進行重排序。

一般重排序可以分為如下三種:

  • 編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序;
  • 指令級並行的重排序。現代處理器采用瞭指令級並行技術來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序;
  • 內存系統的重排序。由於處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行的。

那 Volatile 是怎麼保證不會被執行重排序的呢?

二、內存屏障

java編譯器會在生成指令系列時在適當的位置會插入內存屏障指令來禁止特定類型的處理器重排序。

為瞭實現volatile的內存語義,JMM會限制特定類型的編譯器和處理器重排序,JMM會針對編譯器制定volatile重排序規則表:

是否能重排序第二個操作第一個操作普通讀/寫volatile讀volatile寫普通讀/寫NOvolatile讀NONONOvolatile寫NONO

舉例來說,第三行最後一個單元格的意思是:在程序順序中,當第一個操作為普通變量的讀或寫時,如果第二個操作為volatile寫,則編譯器不能重排序這兩個操作。

從上表我們可以看出:

  • 當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序。這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之後。
  • 當第一個操作是volatile讀時,不管第二個操作是什麼,都不能重排序。這個規則確保volatile讀之後的操作不會被編譯器重排序到volatile讀之前。
  • 當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。

需要註意的是:volatile寫是在前面和後面分別插入內存屏障,而volatile讀操作是在後面插入兩個內存屏障。

深入淺出談談Java並發編程:Volatile

深入淺出談談Java並發編程:Volatile

從JDK5開始,提出瞭happens-before的概念,通過這個概念來闡述操作之間的內存可見性。

三、happens-before

happens-before 關系的定義:

  • 如果一個操作 happens-before 另一個操作,那麼第一個操作的執行結果就會對第二個操作可見。
  • 兩個操作之間如果存在 happens-before 關系,並不意味著 Java 平臺的具體實現就必須按照 happens-before 關系指定的順序來執行。如果重排序之後的執行結果,與按照 happens-before 關系來執行的結果一直,那麼 JMM 也允許這樣的重排序。

看到這兒,你是不是覺得,這個怎麼和 as-if-serial 語義一樣呢。沒錯, happens-before 關系本質上和 as-if-serial 語義是一回事。

as-if-serial 語義保證的是單線程內重排序之後的執行結果和程序代碼本身應該出現的結果是一致的,

happens-before 關系保證的是正確同步的多線程程序的執行結果不會被重排序改變。

一句話來總結就是:如果操作 A happens-before 操作 B ,那麼操作 A 在內存上所做的操作對操作 B 都是可見的,不管它們在不在一個線程。

在 Java 中,對於 happens-before 關系,有以下規定:

  • 程序順序規則:一個線程中的每一個操作, happens-before 於該線程中的任意後續操作。
  • 監視器鎖規則:對一個鎖的解鎖, happens-before 於隨後對這個鎖的加鎖。
  • volatile 變量規則:對一個 volatile 域的寫, happens-before 與任意後續對這個 volatile 域的讀。
  • 傳遞性:如果 A happens-before B , 且 B happens-before C ,那麼 A happens-before C。
  • start 規則:如果線程 A 執行操作 ThreadB。start() 啟動線程 B ,那麼 A 線程的 ThreadB。start() 操作 happens-before 於線程 B 中的任意操作。
  • join 規則:如果線程 A 執行操作 ThreadB。join() 並成功返回,那麼線程 B 中的任意操作 happens-before 於線程 A 從 ThreadB。join() 操作成功返回。

到此這篇關於Java並發編程之Volatile變量詳解分析的文章就介紹到這瞭,更多相關Java Volatile變量內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: