Java 併發編程(三):如何保證共享變量的可見性?

上一篇,咱們談了談如何經過同步來保證共享變量的原子性(一個操做或者多個操做要麼所有執行而且執行的過程不會被任何因素打斷,要麼就都不執行),本篇咱們來談一談如何保證共享變量的可見性(多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值)。java

咱們使用同步的目的不只是,不但願某個線程在使用對象狀態時,另一個線程在修改狀態,這樣容易形成混亂;咱們還但願某個線程修改了對象狀態後,其餘線程可以看到修改後的狀態——這就涉及到了一個新的名詞:內存(可省略)可見性。緩存

要了解可見性,咱們得先來了解一下 Java 內存模型。安全

Java 內存模型(Java Memory Model,簡稱 JMM)描述了 Java 程序中各類變量(線程之間的共享變量)的訪問規則,以及在 JVM 中將變量存儲到內存→從內存中讀取變量的底層細節。ide

要知道,全部的變量都是存儲在主內存中的,每一個線程會有本身獨立的工做內存,裏面保存了該線程使用到的變量副本(主內存中變量的一個拷貝)。見下圖。spa

 

 

也就是說,線程 1 對共享變量 chenmo 的修改要想被線程 2 及時看到,必需要通過 2 個步驟:線程

一、把工做內存 1 中更新過的共享變量刷新到主內存中。
二、將主內存中最新的共享變量的值更新到工做內存 2 中。code

那假如共享變量沒有及時被其餘線程看到的話,會發生什麼問題呢?對象

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 毫秒,也沒有要停下來的意思。blog

爲何會這樣呢?事件

由於主線程對共享變量 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 的更多細節,咱們後面再進行討論。

好了,共享變量的可見性就先介紹到這。但願本篇文章可以對你們有所幫助,謝謝你們的閱讀。

0五、最後

謝謝你們的閱讀,原創不易,喜歡就點個贊,這將是我最強的寫做動力。若是你以爲文章對你有所幫助,也蠻有趣的,就關注一下「沉默王二」公衆號。

相關文章
相關標籤/搜索