深入淺出 Java 并發(fā)編程 (2)

本文目錄

  • Java 內(nèi)存模型與可見性
  • 指令重排序
  • 使用 volatile 關(guān)鍵字保證可見性
  • 使用 synchronized 關(guān)鍵字保證可見性
  • synchronized 和 volatile 關(guān)鍵字的異同

Java 內(nèi)存模型與可見性

上一篇文章主要介紹了 synchronized 關(guān)鍵字的使用,synchronized 關(guān)鍵字本質(zhì)是互斥鎖,保證了程序在不同線程之間執(zhí)行的順序以及同步。對于 Java 程序之中的變量,在不同的線程之中凿歼,還有一個關(guān)鍵的性質(zhì)需要了解:可見性

那么什么是可見性呢鹰溜?

在理解可見性之前我們需要稍微了解一下 Java 的內(nèi)存模型 (JMM)愧沟,所謂 Java 內(nèi)存模型,實際上指的是 Java 用于管理內(nèi)存的一種規(guī)范粤咪,它描述了Java程序中各種變量(線程共享變量)的訪問規(guī)則,以及在 JVM 中將變量存儲到內(nèi)存和從內(nèi)存中讀取變量這樣的底層細(xì)節(jié)渴杆。對于 Java 線程來說寥枝,Java 內(nèi)存模型主要把內(nèi)存分成了兩類:

  • 主內(nèi)存:主要對應(yīng)于Java堆中的對象實例數(shù)據(jù)部分
  • 線程工作內(nèi)存 (本地內(nèi)存):對應(yīng)于虛擬機棧中的部分區(qū)域,是JMM的一個抽象概念磁奖,并不真實存在

在理解這兩個內(nèi)存的時候囊拜,我曾一直想把他們和之前提到過的 堆內(nèi)存 和 棧內(nèi)存 進行比較,但是實際上來說比搭,主內(nèi)存和工作內(nèi)存與堆冠跷、棧內(nèi)存并沒有什么直接的聯(lián)系。關(guān)于這幾種內(nèi)存聯(lián)系的爭論身诺,可以參考這個知乎問答:

JVM中內(nèi)存模型里的『主內(nèi)存』是不是就是指『堆』蜜托,而『工作內(nèi)存』是不是就是指『棧』霉赡?

言歸正傳橄务,我們可以用一個簡單的抽象示意圖來理解 Java 內(nèi)存模型:

Java 內(nèi)存模型抽象示意圖

從上面的圖可以看到,假設(shè)有三個線程Thread1穴亏、Thread2Thread3蜂挪,它們在運行的過程中都會對變量 a 進行一定程度的操作,這些操作都是基于 JMM 給出的規(guī)定:

  • 所有的變量都存儲在主內(nèi)存中
  • 每個線程都有自己獨立的工作內(nèi)存嗓化,里面保存該線程使用到的變量的副本(主內(nèi)存中該變量的一份拷貝)
  • 線程對共享變量的所有操作都必須在自己的工作內(nèi)存中進行棠涮,不能直接從主內(nèi)存中讀寫
  • 不同線程之間無法直接訪問其他線程工作內(nèi)存中的變量,線程間變量值的傳遞需要通過主內(nèi)存來完成刺覆。

也就是說严肪,線程想要對變量 a 進行操作,首先得從主內(nèi)存之中獲取一個 a 的副本隅津,然后在自己的本地內(nèi)存(工作內(nèi)存)之中對 a 的副本進行修改诬垂。當(dāng)修改操作完成以后,再將本地內(nèi)存中的 “新版a” 更新到主內(nèi)存之中伦仍。

說了這么多结窘,這些東西和可見性有什么關(guān)系呢?我們先看下面的圖:

線程之間通信

在圖中充蓝,一開始Thread1Thread2都從主內(nèi)存中獲取了共享變量a的一個副本:a1a2隧枫,它們的初始值滿足:a1 = a2 = a = 0喉磁,但是隨著線程操作的進行,Thread2a2的值改為了1官脓,由于線程1和線程2之間的不可見性协怒,所以造成了a1a2值不一致,為了解決這個問題卑笨,線程2需要把自己修改過的a2先同步到主內(nèi)存中(如圖中紅色箭頭所示)孕暇,然后再經(jīng)由主內(nèi)存刷新到Thread1中,這就是 Java 內(nèi)存模型中線程同步變量的方法赤兴。

所以稍微總結(jié)一下妖滔,可見性指的是在不同的線程之中,一個線程對共享變量值的修改桶良,能夠及時地被其他線程看到座舍。而線程1對共享變量的修改要想被線程2及時看到,必須要經(jīng)過如下2個步驟:

  1. 把工作內(nèi)存1中更新過的共享變量刷新到主內(nèi)存中
  2. 將主內(nèi)存中最新的共享變量的值更新到工作內(nèi)存2中

指令重排序

在多線程環(huán)境里陨帆,除了 Java 線程本地工作內(nèi)存造成的不可見性曲秉,指令重排序也會對線程間的語意和運行結(jié)果造成一定程度的影響。那么疲牵,什么是重排序承二?

以前有一句古話 “所見即所得” ,但是在計算機程序執(zhí)行的時候卻不是這個樣子的瑰步,為了提高程序的性能矢洲,編譯器或處理器會對程序執(zhí)行的順序進行優(yōu)化,使得代碼書寫的順序與實際執(zhí)行的順序未必相同缩焦。

指令重排序

而計算機程序重排序主要又可以分為以下幾類:

  • 編譯器優(yōu)化的重排序(編譯器優(yōu)化)
  • 指令集并行重排序(處理器優(yōu)化)
  • 內(nèi)存系統(tǒng)的重排序(處理器優(yōu)化)

雖然代碼執(zhí)行不一定按照其書寫順序執(zhí)行读虏,但是為了保證在單線程中代碼最終輸出結(jié)果不會因為指令重排序而改變,編譯器袁滥、運行時環(huán)境和處理器都會遵循一定的規(guī)范盖桥,這里主要是指 as-if-serial語義happens- before的程序順序規(guī)則

as-if-serial語義: 不管怎么重排序(編譯器和處理器為了提高并行度)题翻,(單線程)程序的執(zhí)行結(jié)果不能被改變揩徊。

為了遵守as-if-serial語義,編譯器和處理器不會對存在數(shù)據(jù)依賴關(guān)系的操作做重排序嵌赠,因為這種重排序會改變執(zhí)行結(jié)果塑荒。但是,如果操作之間不存在數(shù)據(jù)依賴關(guān)系姜挺,這些操作可能被編譯器和處理器重排序齿税。為了具體說明,我們繼續(xù)使用上面的例子:

int A = 1; // 1
int B = 2; // 2
int C = A + B; // 3

其中第一行和第二行執(zhí)行的結(jié)果之間不存在數(shù)據(jù)的依賴性炊豪,因為第一行第二行的成功運行不需要對方的計算結(jié)果凌箕,但是第三行C的計算結(jié)果卻是依賴于AB的拧篮。這個依賴關(guān)系可以用下面的示意圖表示:

依賴關(guān)系

所以根據(jù)依賴關(guān)系,as-if-serial語義將會允許上述程序的第一行和第二行進行重排序牵舱,而第三行的執(zhí)行一定會放在前兩行程序之后串绩。as-if-serial 語義把單線程程序保護了起來,遵守as-if-serial語義的編譯器芜壁、運行時環(huán)境和處理器共同為編寫單線程程序的程序員們創(chuàng)建了一個幻覺:單線程程序是按程序的順序來執(zhí)行的礁凡。as-if-serial 語義使單線程程序員無需擔(dān)心重排序會干擾他們,也無需擔(dān)心內(nèi)存可見性問題沿盅。

然而在多線程情況下就不是這么簡單的了把篓,指令重排序有可能會導(dǎo)致交叉工作的線程在執(zhí)行完相同的程序之后得到不同的結(jié)果。為此我們可以看一下下面的這個小程序:

public class Test {
    int count = 0;
    boolean running = false;

    public void write() {
        count = 1;                  // 1
        running = true;             // 2
    }

    public void read() {
        if (running) {                // 3
            int result =  count++;    // 4
        }
    }
}

這里我們定義了一個布爾值標(biāo)記 running 腰涧,用來表示變量 count 的值是否已經(jīng)被寫入。我們假設(shè)這里現(xiàn)在有兩個線程(分別為Thread1Thread2)紊浩,Thread1 首先執(zhí)行 write()窖铡,對變量 count 進行寫入,然后Thread2 隨即執(zhí)行read()方法坊谁,那么费彼,當(dāng)Thread2運行到第四行的時候,是否能夠看到Thread1對變量count進行的寫入操作呢口芍?

答案是不一定能夠看得見箍铲。

我們對write()來分析,語句1語句2實際上并沒有數(shù)據(jù)依賴關(guān)系鬓椭,根據(jù)as-if-serial 語義颠猴,這兩行代碼在實際運行的時候很可能會被重排序過。同樣的小染,對read()方法來說翘瓮,if(runnig)int result = count++; 這兩個語句也沒有數(shù)據(jù)依賴關(guān)系,也會被重排序裤翩。那么對于線程Thread1Thread2來說资盅,語句1語句2被重排序的時候,程序執(zhí)行會出現(xiàn)如下的效果:

可能出現(xiàn)的一種執(zhí)行順序

在這種情況下踊赠,count++ 這句話在 Thread2 里面比在 Thread1count = 1 更早得到了執(zhí)行呵扛,相比于重排序之前,這樣得到的 count 最終的值為1筐带,而不進行重排序的話結(jié)果是2今穿,如此一來,重排序在多線程環(huán)境中破壞了原有的語意烫堤。同樣荣赶,對于語句3語句4凤价,大家也可以對重排序是否會導(dǎo)致線程不安全做出類似的分析(先考慮數(shù)據(jù)依賴關(guān)系和控制流程依賴關(guān)系)。

使用 volatile 關(guān)鍵字保證可見性

為了解決 Java 內(nèi)存模型之中多線程變量可見性的問題拔创,在上一篇文章中利诺,我們可以利用synchronized互斥鎖的特性來保證多線程之間的變量可見性。

但是之前也有提到剩燥,synchronized關(guān)鍵字實際上是一種重量級的鎖慢逾,為了在這種情況下優(yōu)化它,我們可以使用volatile關(guān)鍵字灭红。volatile關(guān)鍵字可以修飾變量侣滩,一個被其修飾的變量將會具有如下特性:

  • 保證了不同線程對這個變量進行操作時的可見性(一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的)

  • 禁止進行指令重排序

當(dāng)寫一個volatile變量時变擒,JMM會把該線程對應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存君珠。另外的,當(dāng)讀一個volatile變量時娇斑,JMM會把該線程對應(yīng)的本地內(nèi)存置為無效策添,線程接下來將從主內(nèi)存中讀取共享變量。這也是為什么volatile關(guān)鍵字能夠保證不同線程對同一個變量的可見性毫缆。

關(guān)于volatile的底層實現(xiàn)唯竹,我不打算深究,但是可以簡要的了解一下:如果把加入volatile關(guān)鍵字的代碼和未加入volatile關(guān)鍵字的代碼都生成匯編代碼苦丁,會發(fā)現(xiàn)加入volatile關(guān)鍵字的代碼會多出一個lock前綴指令浸颓。

那這個lock前綴指令是干嘛用的呢?

  • 重排序時不能把后面的指令重排序到內(nèi)存屏障之前的位置
  • 使得本CPU的 cache 寫入內(nèi)存
  • 寫入動作也會引起別的CPU或者別的內(nèi)核無效化其cache旺拉,相當(dāng)于讓新寫入的值對別的線程可見

說了那么多产上,volatile的使用其實很簡單,讓我們一起來看個demo:

public class VolatileUse {

    private volatile boolean running = true; // 對比一下有無 volatile 關(guān)鍵字的時候账阻,運行結(jié)果的差別蒂秘。

    void m() {
        System.out.println("m start...");
        while (running) {

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

    public static void main(String[] args) {
        VolatileUse t = new VolatileUse();
        new Thread(t::m, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        t.running = false;
    }
}

在這個小程序中,如果對 running 加上了 volatile關(guān)鍵字淘太,那么最后處于主線程的操作t.running = false; 將會被 線程t 所看到姻僧,從而打破死循環(huán),使方法m()正常結(jié)束蒲牧。如果不加關(guān)鍵字撇贺,那么程序?qū)⒁恢笨ㄔ?code>m()方法的死循環(huán)中,永遠也不會輸出m end...冰抢。

那么volatile關(guān)鍵字能不能取代synchronized呢松嘶?我們再來看一個demo:

import java.util.ArrayList;
import java.util.List;

/**
 * volatile 關(guān)鍵字,使一個變量在多個線程間可見挎扰。
 * volatile 只有可見性翠订,synchronized 既保證了可見性巢音,又保證了原子性,但是效率遠不如 volatile尽超。
 *
 * @author huangyz0918
 */
public class VolatileUse02 {

    volatile int count = 0;

    void m() {
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }

    public static void main(String[] args) {
        VolatileUse02 t = new VolatileUse02();
        List<Thread> threads = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread-" + i));
        }

        threads.forEach((o) -> o.start());

        threads.forEach((o) -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println(t.count);
    }
}

嘗試運行了一下:

94141

再運行一次:

97096

我們可以看到兩次運行的結(jié)果不同官撼,并且都沒有達到理論上所需要達到的目標(biāo)值:100000。這是為什么呢似谁?(count++語句包含了讀取count的值傲绣,自增,重新賦值操作)

可以這樣理解:有兩個線程 (線程A 和 線程B) 都對變量count進行自加操作巩踏,如果某一個時刻線程 A 讀取了count的值為100秃诵,這時候被阻塞了,因為沒有對變量進行修改塞琼,觸發(fā)不了volatile的規(guī)則菠净。

線程B 此時也讀讀count的值,主內(nèi)存里count的值依舊為100彪杉,做自增嗤练,然后立刻就被寫回主存了,為101在讶。此時又輪到 線程A 執(zhí)行,由于工作內(nèi)存里保存的是100霜大,所以繼續(xù)做自增构哺,再寫回主存,101又被寫了一遍战坤。所以雖然兩個線程執(zhí)行了兩次自增操作曙强,結(jié)果卻只加了一次。

有人說途茫,volatile不是會使緩存行無效的嗎碟嘴?但是這里從線程A開始讀取count的值一直到 線程B 也進行操作之前,并沒有修改count的值囊卜,所以 當(dāng)線程B 讀取的時候娜扇,還是讀的100。

又有人說栅组,線程B將101寫回主內(nèi)存雀瓢,不會把線程A的緩存設(shè)為無效嗎?但是線程A的讀取操作已經(jīng)做過了啊玉掸,只有在做讀取操作時刃麸,發(fā)現(xiàn)自己緩存行無效,才會去讀主內(nèi)存的值司浪,所以這里線程A只能繼續(xù)做自增了泊业。

總的來說把沼,volatile其實是無法完全替代synchronied關(guān)鍵字的,因為在某些復(fù)雜的業(yè)務(wù)邏輯里面吁伺,volatile并不能保證多線程之間的完全同步和操作的原子性饮睬。

使用 synchronized 關(guān)鍵字保證可見性

在看過《深入淺出 Java 并發(fā)編程 (1)》 之后,想必大家都對synchronized關(guān)鍵字同步鎖的性質(zhì)有所了解了箱蝠,但是關(guān)于為什么synchronized關(guān)鍵字能夠保證可見性還需要從synchronized實現(xiàn)的步驟和原理去理解续捂。

在 Java 內(nèi)存模型中,對synchronized關(guān)鍵字有兩條規(guī)定:

  • 線程解鎖前宦搬,必須把共享變量的最新值刷新到主內(nèi)存中牙瓢。

  • 線程加鎖前,將清空工作內(nèi)存中共享變量的值间校,從而使用共享變量時需要從主內(nèi)存中重新讀取最新的值(注意:加鎖和解鎖需要是同一把鎖)矾克。

這兩條規(guī)定保證了線程解鎖前對共享變量的修改在下次加鎖時對其他線程可見,從而實現(xiàn)了可見性憔足,我們再來看一下synchronized加鎖前后代碼具體的實現(xiàn)步驟:

  1. 獲得互斥鎖
  2. 清空工作內(nèi)存
  3. 從主內(nèi)存拷貝變量的最新副本到工作內(nèi)存
  4. 執(zhí)行代碼
  5. 將更改后的共享變量的值刷新到主內(nèi)存
  6. 釋放互斥鎖

保證可見性的步驟顯而易見胁附。

synchronized 和 volatile 關(guān)鍵字的異同

最后我們再來聊一聊這兩個關(guān)鍵字的異同,這在很多互聯(lián)網(wǎng)公司面試的過程中都屬于熱門考點滓彰。

簡要總結(jié)概括如下:

  • volatile 不需要加鎖控妻,比 synchronized 更輕量級,不會阻塞線程揭绑。
  • 從內(nèi)存可見性角度弓候,volatile讀相當(dāng)于加鎖,volatile寫相當(dāng)于解鎖他匪。
  • synchronized 既能保證可見性菇存,又能保證原子性,而volatile只能保證可見性邦蜜,無法保證原子性依鸥。
  • volatile 只能修飾變量,synchronized 還可修飾方法悼沈。

關(guān)于所謂線程阻塞和死鎖以及相關(guān)的問題和解決方法贱迟,我們將在以后的文章中具體介紹。

相關(guān)閱讀:

本教程純屬原創(chuàng)井辆,轉(zhuǎn)載請聲明
本文提供的鏈接若是失效請及時聯(lián)系作者更新

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末关筒,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子杯缺,更是在濱河造成了極大的恐慌蒸播,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異袍榆,居然都是意外死亡胀屿,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門包雀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宿崭,“玉大人,你說我怎么就攤上這事才写∑隙遥” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵赞草,是天一觀的道長讹堤。 經(jīng)常有香客問我,道長厨疙,這世上最難降的妖魔是什么洲守? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮沾凄,結(jié)果婚禮上梗醇,老公的妹妹穿的比我還像新娘。我一直安慰自己撒蟀,他們只是感情好叙谨,可當(dāng)我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著保屯,像睡著了一般唉俗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上配椭,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機與錄音雹姊,去河邊找鬼股缸。 笑死,一個胖子當(dāng)著我的面吹牛吱雏,可吹牛的內(nèi)容都是我干的敦姻。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼歧杏,長吁一口氣:“原來是場噩夢啊……” “哼镰惦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起犬绒,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤旺入,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體茵瘾,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡礼华,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了拗秘。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片圣絮。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖雕旨,靈堂內(nèi)的尸體忽然破棺而出扮匠,到底是詐尸還是另有隱情,我是刑警寧澤凡涩,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布棒搜,位于F島的核電站,受9級特大地震影響突照,放射性物質(zhì)發(fā)生泄漏帮非。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一讹蘑、第九天 我趴在偏房一處隱蔽的房頂上張望末盔。 院中可真熱鬧,春花似錦座慰、人聲如沸陨舱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽游盲。三九已至,卻和暖如春蛮粮,著一層夾襖步出監(jiān)牢的瞬間益缎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工然想, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留莺奔,地道東北人。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓变泄,卻偏偏與公主長得像令哟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子妨蛹,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 今天蛙卤,因為單位沒有發(fā)工資的事狠半,離職員工大鬧單位,在佩服他們?yōu)樽约河赂揖S權(quán)的同事,還想對整件事里各角色不承擔(dān)責(zé)任而導(dǎo)...
    黃家小妞閱讀 131評論 0 0
  • 有教養(yǎng)不是吃飯不灑湯,是別人灑湯的時候別去看他瘤袖∫履Γ——契科夫 01 單位里,我認(rèn)識的一個特別秀氣的女孩子叫昕薇捂敌,每次...
    hello志敏閱讀 626評論 1 3
  • 昨天晚上艾扮,我坐在床邊,抱著女兒占婉,在玩泡嘴, 慧妹看到了,可能是感覺這個角度不錯逆济,拿起手機酌予,咔咔咔,幫我和女兒照了幾張相...
    旦旦日記閱讀 327評論 1 1