1. 程式人生 > >Java 併發程式設計(三):如何保證共享變數的可見性?

Java 併發程式設計(三):如何保證共享變數的可見性?

上一篇,我們談了談如何通過同步來保證共享變數的原子性(一個操作或者多個操作要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行),本篇我們來談一談如何保證共享變數的可見性(多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值)。

我們使用同步的目的不僅是,不希望某個執行緒在使用物件狀態時,另外一個執行緒在修改狀態,這樣容易造成混亂;我們還希望某個執行緒修改了物件狀態後,其他執行緒能夠看到修改後的狀態——這就涉及到了一個新的名詞:記憶體(可省略)可見性。

要了解可見性,我們得先來了解一下 Java 記憶體模型。

Java 記憶體模型(Java Memory Model,簡稱 JMM)描述了 Java 程式中各種變數(執行緒之間的共享變數)的訪問規則,以及在 JVM 中將變數儲存到記憶體→從記憶體中讀取變數的底層細節。

要知道,所有的變數都是儲存在主記憶體中的,每個執行緒會有自己獨立的工作記憶體,裡面儲存了該執行緒使用到的變數副本(主記憶體中變數的一個拷貝)。見下圖。

 

 

也就是說,執行緒 1 對共享變數 chenmo 的修改要想被執行緒 2 及時看到,必須要經過 2 個步驟:

1、把工作記憶體 1 中更新過的共享變數重新整理到主記憶體中。
2、將主記憶體中最新的共享變數的值更新到工作記憶體 2 中。

那假如共享變數沒有及時被其他執行緒看到的話,會發生什麼問題呢?

public class Wanger {
    private static boolean chenmo = false;

    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!chenmo) {
                }
            }
        });
        thread.start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        chenmo = true;

    }

}

這段程式碼的本意是:在主執行緒中建立子執行緒,然後啟動它,當主執行緒休眠 500 毫秒後,把共享變數 chenmo 的值修改為 true 的時候,子執行緒中的 while 迴圈停下來。但執行這段程式碼後,程式似乎進入了死迴圈,過了 N 個 500 毫秒,也沒有要停下來的意思。

為什麼會這樣呢?

因為主執行緒對共享變數 chenmo 的修改沒有及時通知到子執行緒(子執行緒在執行的時候,會將 chenmo 變數的值拷貝一份放在自己的工作記憶體當中),當主執行緒更改了 chenmo 變數的值之後,但是還沒來得及寫入到主存當中,那麼子執行緒此時就不知道主執行緒對 chenmo 變數的更改,因此還會一直迴圈下去。

換句話說,就是:普通的共享變數不能保證可見性,因為普通共享變數被修改之後,什麼時候被寫入主記憶體是不確定的,當其他執行緒去讀取時,此時記憶體中可能還是原來的舊值,因此無法保證可見性。

那怎麼解決這個問題呢?

使用 volatile 關鍵字修飾共享變數 chenmo。

因為 volatile 變數被執行緒訪問時,會強迫執行緒從主記憶體中重讀變數的值,而當變數被執行緒修改時,又會強迫執行緒將最近的值重新整理到主記憶體當中。這樣的話,執行緒在任何時候總能看到變數的最新值。

我們來使用 volatile 修飾一下共享變數 chenmo。

private static volatile boolean chenmo = false;

再次執行程式碼後,程式在一瞬間就結束了,500 毫秒畢竟很短啊。在主執行緒(main 方法)將 chenmo 修改為 true 後,chenmo 變數的值立即寫入到了主記憶體當中;同時,導致子執行緒的工作記憶體中快取變數 chenmo 的副本失效了;當子執行緒讀取 chenmo 變數時,發現自己的快取副本無效了,就會去主記憶體讀取最新的值(由 false 變為 true 了),於是 while 迴圈也就停止了。

也就是說,在某種場景下,我們可以使用 volatile 關鍵字來安全地共享變數。這種場景之一就是:狀態真正獨立於程式內地其他內容,比如一個布林狀態標誌(從 false 到 true,也可以再轉換到 false),用於指示發生了一個重要的一次性事件。

至於 volatile 的原理和實現機制,本篇不再深入展開了(小編自己沒搞懂,尷尬而不失禮貌的笑一笑)。

需要再次強調地是:

volatile 變數可以被看作是一種 “程度較輕的 synchronized”;與 synchronized 相比,volatile 變數執行時地開銷比較少,但是它所能實現的功能也僅是 synchronized 的一部分(只能確保可見性,不能確保原子性)。

原子性我們上一篇已經討論過了,增量操作(i++)看上去像一個單獨操作,但實際上它是一個由“讀取-修改-寫入”組成的序列操作,因此 volatile 並不能為其提供必須的原子特性。

除了 volatile 和 synchronized,Lock 也能夠保證可見性,它能保證同一時刻只有一個執行緒獲取鎖然後執行同步程式碼,並且在釋放鎖之前會將對變數的修改重新整理到主存當中。關於 Lock 的更多細節,我們後面再進行討論。

好了,共享變數的可見性就先介紹到這。希望本篇文章能夠對大家有所幫助,謝謝大家的閱讀。

05、最後

謝謝大家的閱讀,原創不易,喜歡就點個贊,這將是我最強的寫作動力。如果你覺得文章對你有所幫助,也蠻有趣的,就關注一下「沉默王二」公眾號。