最新詳細(xì)的JMM內(nèi)存模型(三天熬夜血肝)

知識(shí)圖譜

image.png

前言

網(wǎng)上并發(fā)以及JMM部分的內(nèi)容大部分都特別的亂仁烹,也不好整理欠痴。花了三四天時(shí)間才整理了一篇秒咨,有些概念的東西喇辽,是需要了解的,也標(biāo)注出來了雨席。

標(biāo)注:在學(xué)習(xí)中需要修改的內(nèi)容以及筆記全在這里 www.javanode.cn菩咨,謝謝!有任何不妥的地方望糾正

并發(fā)編程的優(yōu)缺點(diǎn)

1. 為什么要用到并發(fā)

多核的CPU的背景下陡厘,催生了并發(fā)編程的趨勢(shì)抽米,通過并發(fā)編程的形式可以將多核CPU的計(jì)算能力發(fā)揮到極致,性能得到提升

面對(duì)復(fù)雜業(yè)務(wù)模型糙置,并行程序會(huì)比串行程序更適應(yīng)業(yè)務(wù)需求云茸,而并發(fā)編程更能吻合這種業(yè)務(wù)拆分

2. 并發(fā)編程有哪些缺點(diǎn)

2.1 頻繁的上下文切換

時(shí)間片是CPU分配給各個(gè)線程的時(shí)間,因?yàn)闀r(shí)間非常短谤饭,所以CPU不斷通過切換線程标捺,讓我們覺得多個(gè)線程是同時(shí)執(zhí)行的,時(shí)間片一般是幾十毫秒揉抵。而每次切換時(shí)亡容,需要保存當(dāng)前的狀態(tài)起來,以便能夠進(jìn)行恢復(fù)先前狀態(tài)冤今,而這個(gè)切換時(shí)非常損耗性能闺兢,過于頻繁反而無(wú)法發(fā)揮出多線程編程的優(yōu)勢(shì)通常減少上下文切換可以采用無(wú)鎖并發(fā)編程辟汰,CAS算法列敲,使用最少的線程和使用協(xié)程

2.2 線程安全

多線程編程中最難以把握的就是臨界區(qū)線程安全問題帖汞,稍微不注意就會(huì)出現(xiàn)死鎖的情況戴而,一旦產(chǎn)生死鎖就會(huì)造成系統(tǒng)功能不可用。

public class DeadLockDemo {
    private static String resource_a = "A";
    private static String resource_b = "B";

    public static void main(String[] args) {
        deadLock();
    }

    public static void deadLock() {
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resource_a) {
                    System.out.println("get resource a");
                    try {
                        Thread.sleep(3000);
                        synchronized (resource_b) {
                            System.out.println("get resource b");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resource_b) {
                    System.out.println("get resource b");
                    synchronized (resource_a) {
                        System.out.println("get resource a");
                    }
                }
            }
        });
        threadA.start();
        threadB.start();

    }
}

通臭嬲海可以用如下方式避免死鎖的情況

  1. 避免一個(gè)線程同時(shí)獲得多個(gè)鎖所意;
  2. 避免一個(gè)線程在鎖內(nèi)部占有多個(gè)資源,盡量保證每個(gè)鎖只占用一個(gè)資源
  3. 嘗試使用定時(shí)鎖催首,使用lock.tryLock(timeOut)扶踊,當(dāng)超時(shí)等待時(shí)當(dāng)前線程不會(huì)阻塞;
  4. 對(duì)于數(shù)據(jù)庫(kù)鎖郎任,加鎖和解鎖必須在一個(gè)數(shù)據(jù)庫(kù)連接里秧耗,否則會(huì)出現(xiàn)解鎖失敗的情況

并發(fā)三要素(了解)

可見性: CPU緩存引起

可見性:當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí),如果其中一個(gè)線程對(duì)其作了修改舶治,其他線程能立即獲取到最新的值分井。

原子性: 分時(shí)復(fù)用引起

原子性:即一個(gè)操作或者多個(gè)操作 要么全部執(zhí)行并且執(zhí)行的過程不會(huì)被任何因素打斷车猬,要么就都不執(zhí)行

有序性: 重排序引起

程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。(處理器可能會(huì)對(duì)指令進(jìn)行重排序)

在執(zhí)行程序時(shí)為了提高性能尺锚,編譯器和處理器常常會(huì)對(duì)指令做重排序珠闰。重排序分三種類型

  • 編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語(yǔ)義的前提下瘫辩,可以重新安排語(yǔ)句的執(zhí)行順序伏嗜。
  • 指令級(jí)并行的重排序。現(xiàn)代處理器采用了指令級(jí)并行技術(shù)(Instruction-Level Parallelism伐厌, ILP)來將多條指令重疊執(zhí)行承绸。如果不存在數(shù)據(jù)依賴性,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序弧械。
  • 內(nèi)存系統(tǒng)的重排序八酒。由于處理器使用緩存和讀 / 寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行刃唐。

并發(fā)核心概念(了解)

并發(fā)與并行(重要)

  • 第一種

    • 在單CPU系統(tǒng)中羞迷,系統(tǒng)調(diào)度在某一時(shí)刻只能讓一個(gè)線程運(yùn)行,雖然這種調(diào)試機(jī)制有多種形式(大多數(shù)是時(shí)間片輪巡為主)画饥,但無(wú)論如何衔瓮,要通過不斷切換需要運(yùn)行的線程讓其運(yùn)行的方式就叫并發(fā)(concurrent)。

    • 而在多CPU系統(tǒng)中抖甘,可以讓兩個(gè)以上的線程同時(shí)運(yùn)行热鞍,這種可以同時(shí)讓兩個(gè)以上線程同時(shí)運(yùn)行的方式叫做并行

  • 第二種

你吃飯吃到一半,電話來了衔彻,你一直到吃完了以后才去接薇宠,這就說明你不支持并發(fā)也不支持并行。
你吃飯吃到一半艰额,電話來了澄港,你停了下來接了電話,接完后繼續(xù)吃飯柄沮,這說明你支持并發(fā)回梧。
你吃飯吃到一半,電話來了祖搓,你一邊打電話一邊吃飯狱意,這說明你支持并行。

并發(fā)的關(guān)鍵是你有處理多個(gè)任務(wù)的能力拯欧,不一定要同時(shí)详囤。
并行的關(guān)鍵是你有同時(shí)處理多個(gè)任務(wù)的能力。

關(guān)鍵的點(diǎn)就是:是否是『同時(shí)』镐作。

同步(重要)

在并發(fā)中纬纪,我們可以將同步定義為一種協(xié)調(diào)兩個(gè)或更多任務(wù)以獲得預(yù)期結(jié)果的機(jī)制蚓再。同步的方式有兩種:

  • 控制同步:例如,當(dāng)一個(gè)任務(wù)的開始依賴于另一個(gè)任務(wù)的結(jié)束時(shí)包各,第二個(gè)任務(wù)不能在第一個(gè)任務(wù)完成之前開始。

  • 數(shù)據(jù)訪問同步:當(dāng)兩個(gè)或更多任務(wù)訪問共享變量時(shí)靶庙,在任意時(shí)間里问畅,只有一個(gè)任務(wù)可以訪問該變量。

與同步密切相關(guān)的一個(gè)概念是臨界段六荒。臨界段是一段代碼护姆,由于它可以訪問共享資源,因此在任何給定時(shí)間內(nèi)掏击,只能被一個(gè)任務(wù)執(zhí)行卵皂。互斥是用來保證這一要求的機(jī)制,而且可以采用不同的方式來實(shí)現(xiàn)砚亭。

并發(fā)系統(tǒng)中有不同的同步機(jī)制灯变。從理論角度看,最流行的機(jī)制如下:

  • 信號(hào)量(semaphore):一種用于控制對(duì)一個(gè)或多個(gè)單位資源進(jìn)行訪問的機(jī)制捅膘。它有一個(gè)用于存放可用資源數(shù)量的變量添祸,而且可以采用兩種原子操作來管理該變量⊙罢蹋互斥(mutex刃泌,mutual exclusion的簡(jiǎn)寫形式)是一種特殊類型的信號(hào)量,它只能取兩個(gè)值(即資源空閑資源忙)署尤,而且只有將互斥設(shè)置為忙的那個(gè)進(jìn)程才可以釋放它耙替。互斥可以通過保護(hù)臨界段來幫助你避免出現(xiàn)競(jìng)爭(zhēng)條件曹体。

  • 監(jiān)視器:一種在共享資源上實(shí)現(xiàn)互斥的機(jī)制俗扇。它有一個(gè)互斥、一個(gè)條件變量混坞、兩種操作(等待條件和通報(bào)條件)狐援。一旦你通報(bào)了該條件,在等待它的任務(wù)中只有一個(gè)會(huì)繼續(xù)執(zhí)行究孕。如果共享數(shù)據(jù)的所有用戶都受到同步機(jī)制的保護(hù)啥酱,那么代碼(或方法、對(duì)象)就是線程安全的厨诸。數(shù)據(jù)的非阻塞的CAS(compare-and-swap镶殷,比較和交換)原語(yǔ)是不可變的,這樣就可以在并發(fā)應(yīng)用程序中使用該代碼而不會(huì)出任何問題微酬。

不可變對(duì)象

不可變對(duì)象是一種非常特殊的對(duì)象绘趋。在其初始化后颤陶,不能修改其可視狀態(tài)(其屬性值)。如果想修改一個(gè)不可變對(duì)象陷遮,那么你就必須創(chuàng)建一個(gè)新的對(duì)象滓走。

不可變對(duì)象的主要優(yōu)點(diǎn)在于它是線程安全的。你可以在并發(fā)應(yīng)用程序中使用它而不會(huì)出現(xiàn)任何問題帽馋。

不可變對(duì)象的一個(gè)例子就是java中的String類搅方。當(dāng)你給一個(gè)String對(duì)象賦新值時(shí),會(huì)創(chuàng)建一個(gè)新的String對(duì)象绽族。

原子操作和原子變量

與應(yīng)用程序的其他任務(wù)相比姨涡,原子操作是一種發(fā)生在瞬間的操作。在并發(fā)應(yīng)用程序中吧慢,可以通過一個(gè)臨界段來實(shí)現(xiàn)原子操作涛漂,以便對(duì)整個(gè)操作采用同步機(jī)制。

原子變量是一種通過原子操作來設(shè)置和獲取其值的變量检诗⌒僬蹋可以使用某種同步機(jī)制來實(shí)現(xiàn)一個(gè)原子變量,或者也可以使用CAS以無(wú)鎖方式來實(shí)現(xiàn)一個(gè)原子變量岁诉,而這種方式并不需要任何同步機(jī)制锚沸。

共享內(nèi)存與消息傳遞(重要)

任務(wù)可以通過兩種不同的方式來相互通信。

  • 共享內(nèi)存涕癣,通常用于在同一臺(tái)計(jì)算機(jī)上運(yùn)行多任務(wù)的情況哗蜈。任務(wù)在讀取和寫入值的時(shí)候使用相同的內(nèi)存區(qū)域。為了避免出現(xiàn)問題坠韩,對(duì)該共享內(nèi)存的訪問必須在一個(gè)由同步機(jī)制保護(hù)的臨界段內(nèi)完成距潘。

  • 消息傳遞,通常用于在不同計(jì)算機(jī)上運(yùn)行多任務(wù)的情形只搁。當(dāng)一個(gè)任務(wù)需要與另一個(gè)任務(wù)通信時(shí)音比,它會(huì)發(fā)送一個(gè)遵循預(yù)定義協(xié)議的消息。如果發(fā)送方保持阻塞并等待響應(yīng)氢惋,那么該通信就是同步的洞翩;如果發(fā)送方在發(fā)送消息后繼續(xù)執(zhí)行自己的流程,那么該通信就是異步的焰望。

并發(fā)的問題(了解)

數(shù)據(jù)競(jìng)爭(zhēng)

如果有兩個(gè)或者多個(gè)任務(wù)在臨界段之外對(duì)一個(gè)共享變量進(jìn)行寫入操作骚亿,也就是說沒有使用任何同步機(jī)制,那么應(yīng)用程序可能存在數(shù)據(jù)競(jìng)爭(zhēng)(也叫做競(jìng)爭(zhēng)條件)熊赖。

在這些情況下来屠,應(yīng)用程序的最終結(jié)果可能取決于任務(wù)的執(zhí)行順序。

public class ConcurrentDemo { 
    
    private float myFloat; 
    
    public void modify(float difference) { 
        
        float value = this.myFloat; 
        this.myFloat = value + difference;
        
    }
    
    public static void main(String[] args) {
    } 
}

死鎖

當(dāng)兩個(gè)(或多個(gè))任務(wù)正在等待必須由另一線程釋放的某個(gè)共享資源,而該線程又正在等待必須由前述任務(wù)之一釋放的另一共享資源時(shí)俱笛,并發(fā)應(yīng)用程序就出現(xiàn)了死鎖捆姜。當(dāng)系統(tǒng)中同時(shí)出現(xiàn)如下四種條件時(shí),就會(huì)導(dǎo)致這種情形迎膜。我們將其稱為Coffman 條件泥技。

  • 互斥: 死鎖中涉及的資源、必須是不可共享的星虹。一次只有一個(gè)任務(wù)可以使用該資源零抬。
  • 占有并等待條件: 一個(gè)任務(wù)在占有某一互斥的資源時(shí)又請(qǐng)求另一互斥的資源。當(dāng)它在等待時(shí)宽涌,不會(huì)釋放任何資源。
  • 不可剝奪:資源只能被那些持有它們的任務(wù)釋放蝶棋。
  • 循環(huán)等待:任務(wù)1正等待任務(wù)2 所占有的資源卸亮, 而任務(wù)2 又正在等待任務(wù)3 所占有的資源,以此類推玩裙,最終任務(wù)n又在等待由任務(wù)1所占有的資源兼贸,這樣就出現(xiàn)了循環(huán)等待。

有一些機(jī)制可以用來避免死鎖吃溅。

  • 忽略它們:這是最常用的機(jī)制溶诞。你可以假設(shè)自己的系統(tǒng)絕不會(huì)出現(xiàn)死鎖,而如果發(fā)生死鎖决侈,結(jié)果就是你可以停止應(yīng)用程序并且重新執(zhí)行它螺垢。

  • 檢測(cè):系統(tǒng)中有一項(xiàng)專門分析系統(tǒng)狀態(tài)的任務(wù),可以檢測(cè)是否發(fā)生了死鎖赖歌。如果它檢測(cè)到了死鎖枉圃,可以采取一些措施來修復(fù)該問題,例如庐冯,結(jié)束某個(gè)任務(wù)或者強(qiáng)制釋放某一資源孽亲。

  • 預(yù)防:如果你想防止系統(tǒng)出現(xiàn)死鎖,就必須預(yù)防Coffman 條件中的一條或多條出現(xiàn)

  • 規(guī)避:如果你可以在某一任務(wù)執(zhí)行之前得到該任務(wù)所使用資源的相關(guān)信息展父,那么死鎖是可以規(guī)避的返劲。當(dāng)一個(gè)任務(wù)要開始執(zhí)行時(shí),你可以對(duì)系統(tǒng)中空閑的資源和任務(wù)所需的資源進(jìn)行分析栖茉,這樣就可以判斷任務(wù)是否能夠開始執(zhí)行篮绿。

活鎖

如果系統(tǒng)中有兩個(gè)任務(wù),它們總是因?qū)Ψ降男袨槎淖冏约旱臓顟B(tài)衡载, 那么就出現(xiàn)了活鎖搔耕。最終結(jié)果是它們陷入了狀態(tài)變更的循環(huán)而無(wú)法繼續(xù)向下執(zhí)行。

例如,有兩個(gè)任務(wù):任務(wù)1和任務(wù)2 弃榨,它們都需要用到兩個(gè)資源:資源1和資源2 菩收。假設(shè)任務(wù)1對(duì)資源1加了一個(gè)鎖,而任務(wù)2 對(duì)資源2 加了一個(gè)鎖鲸睛。當(dāng)它們無(wú)法訪問所需的資源時(shí)娜饵,就會(huì)釋放自己的資源并且重新開始循環(huán)。這種情況可以無(wú)限地持續(xù)下去官辈,所以這兩個(gè)任務(wù)都不會(huì)結(jié)束自己的執(zhí)行過程箱舞。

資源不足

當(dāng)某個(gè)任務(wù)在系統(tǒng)中無(wú)法獲取維持其繼續(xù)執(zhí)行所需的資源時(shí),就會(huì)出現(xiàn)資源不足拳亿。當(dāng)有多個(gè)任務(wù)在等待某一資源且該資源被釋放時(shí)晴股,系統(tǒng)需要選擇下一個(gè)可以使用該資源的任務(wù)。如果你的系統(tǒng)中沒有設(shè)計(jì)良好的算法肺魁,那么系統(tǒng)中有些線程很可能要為獲取該資源而等待很長(zhǎng)時(shí)間电湘。

要解決這一問題就要確保公平原則。所有等待某一資源的任務(wù)必須在某一給定時(shí)間之內(nèi)占有該資源鹅经〖徘海可選方案之一就是實(shí)現(xiàn)一個(gè)算法,在選擇下一個(gè)將占有某一資源的任務(wù)時(shí)瘾晃,對(duì)任務(wù)已等待該資源的時(shí)間因素加以考慮贷痪。然而,實(shí)現(xiàn)鎖的公平需要增加額外的開銷蹦误,這可能會(huì)降低程序的吞吐量劫拢。

優(yōu)先權(quán)反轉(zhuǎn)

當(dāng)一個(gè)低優(yōu)先權(quán)的任務(wù)持有了一個(gè)高優(yōu)先級(jí)任務(wù)所需的資源時(shí),就會(huì)發(fā)生優(yōu)先權(quán)反轉(zhuǎn)胖缤。這樣的話尚镰,低優(yōu)先權(quán)的任務(wù)就會(huì)在高優(yōu)先權(quán)的任務(wù)之前執(zhí)行。

java內(nèi)存模型(JMM) 重要

JMM概述

出現(xiàn)線程安全的問題一般是因?yàn)?strong>主內(nèi)存和工作內(nèi)存數(shù)據(jù)不一致性和重排序導(dǎo)致的哪廓,而解決線程安全的問題最重要的就是理解這兩種問題是怎么來的狗唉,那么,理解它們的核心在于理解java內(nèi)存模型(JMM)涡真。

Java 的并發(fā)采用的是共享內(nèi)存模型分俯,Java 線程之間的通信總是隱式進(jìn)行,整個(gè)通信過程對(duì)程序員完全透明哆料。如果編寫多線程程序的 Java 程序員不理解隱式進(jìn)行的線程之間通信的工作機(jī)制缸剪,很可能會(huì)遇到各種奇怪的內(nèi)存可見性問題。我們需要處理兩個(gè)關(guān)鍵問題:線程之間如何通信及線程之間如何同步(這里的線程是指并發(fā)執(zhí)行的活動(dòng)實(shí)體)东亦。通信是指線程之間以何種機(jī)制來交換信息杏节。緊接著我們需要知道java中那些是共享內(nèi)存

共享變量與局部變量

  • 共享變量:在 java 中唬渗,所有實(shí)例域、靜態(tài)域和數(shù)組元素存儲(chǔ)在堆內(nèi)存中奋渔,堆內(nèi)存在線程之間共享镊逝。

  • 局部變量(Local variables), 方法定義參數(shù)(java 語(yǔ)言規(guī)范稱之為 formal method parameters)和異常處理器參數(shù)(exception handler parameters)不會(huì)在線程之間共享,它們不會(huì)有內(nèi)存可見性問題嫉鲸,也不受內(nèi)存模型的影響撑蒜。

JMM內(nèi)存模型抽象

Java 線程之間的通信由 Java 內(nèi)存模型(JMM java method model)控制,JMM 決定一個(gè)線程對(duì)共享變量的寫入何時(shí)對(duì)另一個(gè)線程可見玄渗。從抽象的角度來看座菠,JMM 定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲(chǔ)在主內(nèi)存(main memory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(local memory)藤树,本地內(nèi)存中存儲(chǔ)了該線程以讀 / 寫共享變量的副本浴滴。本地內(nèi)存是 JMM 的一個(gè)抽象概念,并不真實(shí)存在岁钓。它涵蓋了緩存巡莹,寫緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化甜紫。

Java 內(nèi)存模型的抽象示意圖如下:

image.png

從上圖來看,線程 A 與線程 B 之間如要通信的話骂远,必須要經(jīng)歷下面 2 個(gè)步驟:

  • 首先囚霸,線程 A 把本地內(nèi)存 A 中更新過的共享變量刷新到主內(nèi)存中去。
  • 然后激才,線程 B 到主內(nèi)存中去讀取線程 A 之前已更新過的共享變量拓型。

線程A和線程B通過共享變量在進(jìn)行隱式通信。如果線程A更新后數(shù)據(jù)并沒有及時(shí)寫回到主存瘸恼,而此時(shí)線程B讀到的是過期的數(shù)據(jù)劣挫,這就出現(xiàn)了“臟讀”現(xiàn)象《В可以通過同步機(jī)制(控制不同線程間操作發(fā)生的相對(duì)順序)來解決或者通過volatile關(guān)鍵字使得每次volatile變量都能夠強(qiáng)制刷新到主存压固,從而對(duì)每個(gè)線程都是可見的。

重排序(重要)

一個(gè)好的內(nèi)存模型實(shí)際上會(huì)放松對(duì)處理器和編譯器規(guī)則的束縛靠闭,也就是說軟件技術(shù)和硬件技術(shù)都為同一個(gè)目標(biāo)而進(jìn)行奮斗:在不改變程序執(zhí)行結(jié)果的前提下帐我,盡可能提高并行度。JMM對(duì)底層盡量減少約束愧膀,使其能夠發(fā)揮自身優(yōu)勢(shì)拦键。因此,在執(zhí)行程序時(shí)檩淋,為了提高性能芬为,編譯器和處理器常常會(huì)對(duì)指令進(jìn)行重排序

Store Buffer的延遲寫入是重排序的一種,稱為內(nèi)存重排序(Memory Ordering)媚朦。除此之外氧敢,還有編譯器和CPU的指令重排序。

image.png
  1. 編譯器重排序莲镣。

    對(duì)于沒有先后依賴關(guān)系的語(yǔ)句福稳,編譯器可以重新調(diào)整語(yǔ)句的執(zhí)行順序。

  2. CPU指令重排序瑞侮。

    在指令級(jí)別的圆,讓沒有依賴關(guān)系的多條指令并行。

  3. CPU內(nèi)存重排序半火。

    CPU有自己的緩存越妈,指令的執(zhí)行順序和寫入主內(nèi)存的順序不完全一致。

1屬于編譯器重排序钮糖,而2和3統(tǒng)稱為CPU處理器重排序梅掠。這些重排序會(huì)導(dǎo)致線程安全的問題,一個(gè)很經(jīng)典的例子就是DCL問題.

image.png

假設(shè):X店归、Y是兩個(gè)全局變量阎抒,初始的時(shí)候,X消痛,Y是全局變量并 X=0且叁,Y=0。 線程A,B 分別執(zhí)行各自的值秩伞。線程1和線程2的執(zhí)行先后順序是不確定的逞带,可能順序執(zhí)行,也可能交叉執(zhí)行纱新,這就造成內(nèi)存可見性問題展氓。可能會(huì)出現(xiàn)結(jié)果可能是:

  1. a=0,b=1
  2. a=1,b=0
  3. a=1,b=1

對(duì)于編譯器脸爱,JMM 的編譯器重排序規(guī)則會(huì)禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)遇汞。對(duì)于CPU處理器重排序,JMM 的處理器重排序規(guī)則會(huì)要求 java 編譯器在生成指令序列時(shí)阅羹,插入特定類型的內(nèi)存屏障指令勺疼,通過內(nèi)存屏障指令來禁止特定類型的處理器重排序`(不是所有的處理器重排序都要禁止)。

內(nèi)存屏障(了解)

為了禁止編譯器重排序和 CPU 重排序捏鱼,在編譯器和 CPU 層面都有對(duì)應(yīng)的指令执庐,也就是內(nèi)存屏障(Memory Barrier)。這也正是JMM和happen-before規(guī)則的底層實(shí)現(xiàn)原理导梆。

編譯器的內(nèi)存屏障轨淌,只是為了告訴編譯器不要對(duì)指令進(jìn)行重排序迂烁。當(dāng)編譯完成之后,這種內(nèi)存屏障就消失了递鹉,CPU并不會(huì)感知到編譯器中內(nèi)存屏障的存在盟步。

而CPU的內(nèi)存屏障是CPU提供的指令,可以由開發(fā)者顯示調(diào)用躏结。內(nèi)存屏障是很底層的概念却盘,對(duì)于 Java 開發(fā)者來說,一般用 volatile 關(guān)鍵字就足夠了媳拴。但從JDK 8開始黄橘,Java在Unsafe類中提供了三個(gè)內(nèi)存屏障函數(shù),如下所示屈溉。

public final class Unsafe { 
    // ... 
    public native void loadFence(); 
    public native void storeFence(); 
    public native void fullFence();
    // ...
}

在理論層面塞关,可以把基本的CPU內(nèi)存屏障分成四種:

  1. LoadLoad:禁止讀和讀的重排序。

  2. StoreStore:禁止寫和寫的重排序子巾。

  3. LoadStore:禁止讀和寫的重排序帆赢。

  4. StoreLoad:禁止寫和讀的重排序。

Unsafe中的方法:

  1. loadFence=LoadLoad+LoadStore

  2. storeFence=StoreStore+LoadStore

  3. fullFence=loadFence+storeFence+StoreLoad

as-if-serial語(yǔ)義(了解)

as-if-serial語(yǔ)義的意思是:不管怎么重排序(編譯器和處理器為了提供并行度)线梗,(單線程)程序的執(zhí)行結(jié)果不能被改變椰于。

重排序的原則是什么?什么場(chǎng)景下可以重排序仪搔,什么場(chǎng)景下不能重排序呢廉羔?

  1. 單線程程序的重排序規(guī)則

無(wú)論什么語(yǔ)言,站在編譯器和CPU的角度來說僻造,不管怎么重排序,單線程程序的執(zhí)行結(jié)果不能改變孩饼,這就是單線程程序的重排序規(guī)則髓削。

即只要操作之間沒有數(shù)據(jù)依賴性,編譯器和CPU都可以任意重排序镀娶,因?yàn)閳?zhí)行結(jié)果不會(huì)改變立膛,代碼看起來就像是完全串行地一行行從頭執(zhí)行到尾,這也就是as-if-serial語(yǔ)義梯码。

對(duì)于單線程程序來說宝泵,編譯器和CPU可能做了重排序,但開發(fā)者感知不到轩娶,也不存在內(nèi)存可見性問題儿奶。

  1. 多線程程序的重排序規(guī)則

編譯器和CPU的這一行為對(duì)于單線程程序沒有影響,但對(duì)多線程程序卻有影響鳄抒。

對(duì)于多線程程序來說闯捎,線程之間的數(shù)據(jù)依賴性太復(fù)雜椰弊,編譯器和CPU沒有辦法完全理解這種依賴性并據(jù)此做出最合理的優(yōu)化。

編譯器和CPU只能保證每個(gè)線程的as-if-serial語(yǔ)義瓤鼻。

線程之間的數(shù)據(jù)依賴和相互影響秉版,需要編譯器和CPU的上層來確定。

上層要告知編譯器和CPU在多線程場(chǎng)景下什么時(shí)候可以重排序茬祷,什么時(shí)候不能重排序清焕。

happens-before定義

JMM可以通過happens-before關(guān)系向程序員提供跨線程的內(nèi)存可見性保證這兩個(gè)操作既可以是在一個(gè)線程之內(nèi),也可以是在不同線程之間祭犯。


  • 如果一個(gè)操作happens-before另一個(gè)操作秸妥,那么第一個(gè)操作的執(zhí)行結(jié)果將對(duì)第二個(gè)操作可見,而且第一個(gè)操作的執(zhí)行順序排在第二個(gè)操作之前盹憎。

  • 兩個(gè)操作之間存在happens-before關(guān)系筛峭,并不意味著Java平臺(tái)的具體實(shí)現(xiàn)必須要按照happens-before關(guān)系指定的順序來執(zhí)行。如果重排序之后的執(zhí)行結(jié)果,與按happens-before關(guān)系來執(zhí)行的結(jié)果一致九榔,那么這種重排序并不非法(也就是說蒜魄,JMM允許這種重排序)。

上面的1)是JMM對(duì)程序員的承諾挂签。從程序員的角度來說,可以這樣理解happens-before關(guān)系:如果A happens-before B盼产,那么Java內(nèi)存模型將向程序員保證——A操作的結(jié)果將對(duì)B可見饵婆,且A的執(zhí)行順序排在B之前。注意戏售,這只是Java內(nèi)存模型向程序員做出的保證侨核!

上面的2)是JMM對(duì)編譯器和處理器重排序的約束原則。正如前面所言灌灾,JMM其實(shí)是在遵循一個(gè)基本原則:只要不改變程序的執(zhí)行結(jié)果(指的是單線程程序和正確同步的多線程程序)搓译,編譯器和處理器怎么優(yōu)化都行。JMM這么做的原因是:程序員對(duì)于這兩個(gè)操作是否真的被重排序并不關(guān)心锋喜,程序員關(guān)心的是程序執(zhí)行時(shí)的語(yǔ)義不能被改變(即執(zhí)行結(jié)果不能被改變)些己。因此,happens-before關(guān)系本質(zhì)上和as-if-serial語(yǔ)義是一回事嘿般。


as-if-serial和happens-before的區(qū)別

  1. as-if-serial語(yǔ)義保證單線程內(nèi)程序的執(zhí)行結(jié)果不被改變段标,happens-before關(guān)系保證正確同步的多線程程序的執(zhí)行結(jié)果不被改變。
  2. as-if-serial語(yǔ)義給編寫單線程程序的程序員創(chuàng)造了一個(gè)幻境:?jiǎn)尉€程程序是按程序的順序來執(zhí)行的炉奴。happens-before關(guān)系給編寫正確同步的多線程程序的程序員創(chuàng)造了一個(gè)幻境:正確同步的多線程程序是按happens-before指定的順序來執(zhí)行的逼庞。
  3. as-if-serial語(yǔ)義和happens-before這么做的目的,都是為了在不改變程序執(zhí)行結(jié)果的前提下瞻赶,盡可能地提高程序執(zhí)行的并行度往堡。

happens-before規(guī)則(了解)

  1. 程序順序規(guī)則:一個(gè)線程中的每個(gè)操作械荷,happens-before于該線程中的任意后續(xù)操作。
  2. 監(jiān)視器鎖規(guī)則:對(duì)一個(gè)鎖的解鎖虑灰,happens-before于隨后對(duì)這個(gè)鎖的加鎖吨瞎。
  3. volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫,happens-before于任意后續(xù)對(duì)這個(gè)volatile域的讀穆咐。
  4. 傳遞性:如果A happens-before B颤诀,且B happens-before C,那么A happens-before C对湃。
  5. start()規(guī)則:如果線程A執(zhí)行操作ThreadB.start()(啟動(dòng)線程B)崖叫,那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作。
  6. join()規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回拍柒,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回心傀。
  7. 程序中斷規(guī)則:對(duì)線程interrupted()方法的調(diào)用先行于被中斷線程的代碼檢測(cè)到中斷時(shí)間的發(fā)生。
  8. 對(duì)象finalize規(guī)則:一個(gè)對(duì)象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行于發(fā)生它的finalize()方法的開始拆讯。

happens-before值傳遞(了解)

這些基本的happen-before規(guī)則脂男,happen-before還具有傳遞性,即若A happen-before B种呐,Bhappen-before C宰翅,則A happen-before C。

舉例:

  • volatile

如果一個(gè)變量不是volatile變量爽室,當(dāng)一個(gè)線程讀取汁讼、一個(gè)線程寫入時(shí)可能有問題。那豈不是說阔墩,在多線程程序中嘿架,我們要么加鎖,要么必須把所有變量都聲明為volatile變量啸箫?這顯然不可能眶明,而這就得歸功于happen-before的傳遞性

class A { 
    private int a = 0; 
    private volatile int c = 0; 
    public void set() { 
        a = 5; // 操作1 
        c = 1; // 操作2 
    }
    public int get() { 
        int d = c; // 操作3 
        return a; // 操作4 
    } 
}

? 操作1和操作2是在同一個(gè)線程內(nèi)存中執(zhí)行的筐高,操作1 happen-before 操作2,同理丑瞧,操作3 happen,before操作4柑土。又因?yàn)閏是volatile變量,對(duì)c的寫入happen-before對(duì)c的讀取绊汹,所以操作2 happen稽屏,before操作3。利用happen-before的傳遞性西乖,就得到:

? 操作1 happen-before 操作2 happen-before 操作3 happen-before操作4狐榔。

  • synchronized

因?yàn)榕cvolatile一樣坛增,synchronized同樣具有happen-before語(yǔ)義。展開上面的代碼可得到類似于下面的偽代碼:

class A { 
    private int a = 0; 
    private int c = 0; 
    public synchronized void set() {
        a = 5; // 操作1 
        c = 1; // 操作2 
    }
    public synchronized int get() { 
        return a; 
    } 
}

JMM的設(shè)計(jì)(重要)

上面已經(jīng)聊了關(guān)于JMM的兩個(gè)方面:1. JMM的抽象結(jié)構(gòu)(主內(nèi)存和線程工作內(nèi)存)薄腻;2. 重排序以及happens-before規(guī)則收捣。

image.png
  • 上層會(huì)有基于JMM的關(guān)鍵字和J.U.C包下的一些具體類用來方便程序員能夠迅速高效率的進(jìn)行并發(fā)編程。
  • JMM處于中間層庵楷,包含了兩個(gè)方面:1. 內(nèi)存模型罢艾;2.重排序以及happens-before規(guī)則。為了禁止特定類型的重排序會(huì)對(duì)編譯器和處理器指令序列加以控制尽纽。

在設(shè)計(jì)JMM時(shí)需要考慮兩個(gè)關(guān)鍵因素:

  • 程序員對(duì)內(nèi)存模型的使用 程序員希望內(nèi)存模型易于理解咐蚯、易于編程。程序員希望基于一個(gè)強(qiáng)內(nèi)存模型來編寫代碼
  • 編譯器和處理器對(duì)內(nèi)存模型的實(shí)現(xiàn) 編譯器和處理器希望內(nèi)存模型對(duì)它們的束縛越少越好弄贿,這樣它們就可以做盡可能多的優(yōu)化來提高性能春锋。編譯器和處理器希望實(shí)現(xiàn)一個(gè)弱內(nèi)存模型。

JMM 把 happens- before 要求禁止的重排序分為了下面兩類:

  • 會(huì)改變程序執(zhí)行結(jié)果的重排序差凹。
  • 不會(huì)改變程序執(zhí)行結(jié)果的重排序期奔。

JMM 對(duì)這兩種不同性質(zhì)的重排序,采取了不同的策略:

  • 對(duì)于會(huì)改變程序執(zhí)行結(jié)果的重排序直奋,JMM 要求編譯器和處理器必須禁止這種重排序能庆。
  • 對(duì)于不會(huì)改變程序執(zhí)行結(jié)果的重排序,JMM 對(duì)編譯器和處理器不作要求(JMM 允許這種重排序)
image.png

從上圖可以看出兩點(diǎn):

  • JMM 向程序員提供的 happens- before 規(guī)則能滿足程序員的需求脚线。JMM 的 happens- before 規(guī)則不但簡(jiǎn)單易懂搁胆,而且也向程序員提供了足夠強(qiáng)的內(nèi)存可見性保證(有些內(nèi)存可見性保證其實(shí)并不一定真實(shí)存在,比如上面的 A happens- before B)邮绿。
  • JMM 對(duì)編譯器和處理器的束縛已經(jīng)盡可能的少渠旁。從上面的分析我們可以看出,JMM 其實(shí)是在遵循一個(gè)基本原則:只要不改變程序的執(zhí)行結(jié)果(指的是單線程程序和正確同步的多線程程序)船逮,編譯器和處理器怎么優(yōu)化都行顾腊。比如,如果編譯器經(jīng)過細(xì)致的分析后挖胃,認(rèn)定一個(gè)鎖只會(huì)被單個(gè)線程訪問杂靶,那么這個(gè)鎖可以被消除。再比如酱鸭,如果編譯器經(jīng)過細(xì)致的分析后吗垮,認(rèn)定一個(gè) volatile 變量?jī)H僅只會(huì)被單個(gè)線程訪問,那么編譯器可以把這個(gè) volatile 變量當(dāng)作一個(gè)普通變量來對(duì)待凹髓。這些優(yōu)化既不會(huì)改變程序的執(zhí)行結(jié)果烁登,又能提高程序的執(zhí)行效率。

JMM 的內(nèi)存可見性保證(重要)

Java 程序的內(nèi)存可見性保證按程序類型可以分為下列三類:

  • 單線程程序蔚舀。單線程程序不會(huì)出現(xiàn)內(nèi)存可見性問題饵沧。編譯器锨络,runtime 和處理器會(huì)共同確保單線程程序的執(zhí)行結(jié)果與該程序在順序一致性模型中的執(zhí)行結(jié)果相同。
  • 正確同步的多線程程序狼牺。正確同步的多線程程序的執(zhí)行將具有順序一致性(程序的執(zhí)行結(jié)果與該程序在順序一致性內(nèi)存模型中的執(zhí)行結(jié)果相同)羡儿。這是 JMM 關(guān)注的重點(diǎn),JMM 通過限制編譯器和處理器的重排序來為程序員提供內(nèi)存可見性保證锁右。
  • 未同步 / 未正確同步的多線程程序失受。JMM 為它們提供了最小安全性保障:線程執(zhí)行時(shí)讀取到的值,要么是之前某個(gè)線程寫入的值咏瑟,要么是默認(rèn)值(0拂到,null,false)码泞。

下圖展示了這三類程序在 JMM 中與在順序一致性內(nèi)存模型中的執(zhí)行結(jié)果的異同:

image.png

標(biāo)注:在學(xué)習(xí)中需要修改的內(nèi)容以及筆記全在這里 www.javanode.cn兄旬,謝謝!有任何不妥的地方望糾正

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末余寥,一起剝皮案震驚了整個(gè)濱河市领铐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌宋舷,老刑警劉巖绪撵,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異祝蝠,居然都是意外死亡音诈,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門绎狭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來细溅,“玉大人,你說我怎么就攤上這事儡嘶±模” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵蹦狂,是天一觀的道長(zhǎng)誓篱。 經(jīng)常有香客問我,道長(zhǎng)凯楔,這世上最難降的妖魔是什么窜骄? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮啼辣,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘御滩。我一直安慰自己鸥拧,他們只是感情好党远,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著富弦,像睡著了一般沟娱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上腕柜,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天济似,我揣著相機(jī)與錄音,去河邊找鬼盏缤。 笑死砰蠢,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的唉铜。 我是一名探鬼主播台舱,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼潭流!你這毒婦竟也來了竞惋?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤灰嫉,失蹤者是張志新(化名)和其女友劉穎拆宛,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體讼撒,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡浑厚,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了椿肩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瞻颂。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖郑象,靈堂內(nèi)的尸體忽然破棺而出贡这,到底是詐尸還是另有隱情,我是刑警寧澤厂榛,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布盖矫,位于F島的核電站,受9級(jí)特大地震影響击奶,放射性物質(zhì)發(fā)生泄漏辈双。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一柜砾、第九天 我趴在偏房一處隱蔽的房頂上張望湃望。 院中可真熱鬧,春花似錦、人聲如沸证芭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)废士。三九已至叫潦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間官硝,已是汗流浹背矗蕊。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留氢架,地道東北人傻咖。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像达箍,于是被迫代替她去往敵國(guó)和親没龙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

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