最新详细的JMM内存模型(三天熬夜血肝)
知識(shí)圖譜
前言
網(wǎng)上并發(fā)以及JMM部分的內(nèi)容大部分都特別的亂,也不好整理。花了三四天時(shí)間才整理了一篇,有些概念的東西,是需要了解的,也標(biāo)注出來(lái)了。
標(biāo)注:在學(xué)習(xí)中需要修改的內(nèi)容以及筆記全在這里 www.javanode.cn,謝謝!有任何不妥的地方望糾正
并發(fā)編程的優(yōu)缺點(diǎn)
1. 為什么要用到并發(fā)
多核的CPU的背景下,催生了并發(fā)編程的趨勢(shì),通過(guò)并發(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不斷通過(guò)切換線程,讓我們覺(jué)得多個(gè)線程是同時(shí)執(zhí)行的,時(shí)間片一般是幾十毫秒。而每次切換時(shí),需要保存當(dāng)前的狀態(tài)起來(lái),以便能夠進(jìn)行恢復(fù)先前狀態(tài),而這個(gè)切換時(shí)非常損耗性能,過(guò)于頻繁反而無(wú)法發(fā)揮出多線程編程的優(yōu)勢(shì)。通常減少上下文切換可以采用無(wú)鎖并發(fā)編程,CAS算法,使用最少的線程和使用協(xié)程。
2.2 線程安全
多線程編程中最難以把握的就是臨界區(qū)線程安全問(wèn)題,稍微不注意就會(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() {@Overridepublic 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() {@Overridepublic void run() {synchronized (resource_b) {System.out.println("get resource b");synchronized (resource_a) {System.out.println("get resource a");}}}});threadA.start();threadB.start();} }通常可以用如下方式避免死鎖的情況
并發(fā)三要素(了解)
可見(jiàn)性: CPU緩存引起
可見(jiàn)性:當(dāng)多個(gè)線程訪問(wèn)同一個(gè)變量時(shí),如果其中一個(gè)線程對(duì)其作了修改,其他線程能立即獲取到最新的值。
原子性: 分時(shí)復(fù)用引起
原子性:即一個(gè)操作或者多個(gè)操作 要么全部執(zhí)行并且執(zhí)行的過(guò)程不會(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)來(lái)將多條指令重疊執(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ú)論如何,要通過(guò)不斷切換需要運(yùn)行的線程讓其運(yùn)行的方式就叫并發(fā)(concurrent)。
-
而在多CPU系統(tǒng)中,可以讓兩個(gè)以上的線程同時(shí)運(yùn)行,這種可以同時(shí)讓兩個(gè)以上線程同時(shí)運(yùn)行的方式叫做并行
-
-
第二種
你吃飯吃到一半,電話來(lái)了,你一直到吃完了以后才去接,這就說(shuō)明你不支持并發(fā)也不支持并行。
你吃飯吃到一半,電話來(lái)了,你停了下來(lái)接了電話,接完后繼續(xù)吃飯,這說(shuō)明你支持并發(fā)。
你吃飯吃到一半,電話來(lái)了,你一邊打電話一邊吃飯,這說(shuō)明你支持并行。
并發(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ù)的開(kāi)始依賴于另一個(gè)任務(wù)的結(jié)束時(shí),第二個(gè)任務(wù)不能在第一個(gè)任務(wù)完成之前開(kāi)始。
-
數(shù)據(jù)訪問(wèn)同步:當(dāng)兩個(gè)或更多任務(wù)訪問(wèn)共享變量時(shí),在任意時(shí)間里,只有一個(gè)任務(wù)可以訪問(wèn)該變量。
與同步密切相關(guān)的一個(gè)概念是臨界段。臨界段是一段代碼,由于它可以訪問(wèn)共享資源,因此在任何給定時(shí)間內(nèi),只能被一個(gè)任務(wù)執(zhí)行。互斥是用來(lái)保證這一要求的機(jī)制,而且可以采用不同的方式來(lái)實(shí)現(xiàn)。
并發(fā)系統(tǒng)中有不同的同步機(jī)制。從理論角度看,最流行的機(jī)制如下:
-
信號(hào)量(semaphore):一種用于控制對(duì)一個(gè)或多個(gè)單位資源進(jìn)行訪問(wèn)的機(jī)制。它有一個(gè)用于存放可用資源數(shù)量的變量,而且可以采用兩種原子操作來(lái)管理該變量。互斥(mutex,mutual exclusion的簡(jiǎn)寫形式)是一種特殊類型的信號(hào)量,它只能取兩個(gè)值(即資源空閑和資源忙),而且只有將互斥設(shè)置為忙的那個(gè)進(jìn)程才可以釋放它。互斥可以通過(guò)保護(hù)臨界段來(lái)幫助你避免出現(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ì)出任何問(wèn)題。
不可變對(duì)象
不可變對(duì)象是一種非常特殊的對(duì)象。在其初始化后,不能修改其可視狀態(tài)(其屬性值)。如果想修改一個(gè)不可變對(duì)象,那么你就必須創(chuàng)建一個(gè)新的對(duì)象。
不可變對(duì)象的主要優(yōu)點(diǎn)在于它是線程安全的。你可以在并發(fā)應(yīng)用程序中使用它而不會(huì)出現(xiàn)任何問(wèn)題。
不可變對(duì)象的一個(gè)例子就是java中的String類。當(dāng)你給一個(gè)String對(duì)象賦新值時(shí),會(huì)創(chuàng)建一個(gè)新的String對(duì)象。
原子操作和原子變量
與應(yīng)用程序的其他任務(wù)相比,原子操作是一種發(fā)生在瞬間的操作。在并發(fā)應(yīng)用程序中,可以通過(guò)一個(gè)臨界段來(lái)實(shí)現(xiàn)原子操作,以便對(duì)整個(gè)操作采用同步機(jī)制。
原子變量是一種通過(guò)原子操作來(lái)設(shè)置和獲取其值的變量。可以使用某種同步機(jī)制來(lái)實(shí)現(xiàn)一個(gè)原子變量,或者也可以使用CAS以無(wú)鎖方式來(lái)實(shí)現(xiàn)一個(gè)原子變量,而這種方式并不需要任何同步機(jī)制。
共享內(nèi)存與消息傳遞(重要)
任務(wù)可以通過(guò)兩種不同的方式來(lái)相互通信。
-
共享內(nèi)存,通常用于在同一臺(tái)計(jì)算機(jī)上運(yùn)行多任務(wù)的情況。任務(wù)在讀取和寫入值的時(shí)候使用相同的內(nèi)存區(qū)域。為了避免出現(xiàn)問(wèn)題,對(duì)該共享內(nèi)存的訪問(wèn)必須在一個(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ā)的問(wèn)題(了解)
數(shù)據(jù)競(jìng)爭(zhēng)
如果有兩個(gè)或者多個(gè)任務(wù)在臨界段之外對(duì)一個(gè)共享變量進(jìn)行寫入操作,也就是說(shuō)沒(méi)有使用任何同步機(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ī)制可以用來(lái)避免死鎖。
-
忽略它們:這是最常用的機(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è)到了死鎖,可以采取一些措施來(lái)修復(fù)該問(wèn)題,例如,結(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ù)要開(kāi)始執(zhí)行時(shí),你可以對(duì)系統(tǒng)中空閑的資源和任務(wù)所需的資源進(jìn)行分析,這樣就可以判斷任務(wù)是否能夠開(kāi)始執(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ú)法訪問(wèn)所需的資源時(shí),就會(huì)釋放自己的資源并且重新開(kāi)始循環(huán)。這種情況可以無(wú)限地持續(xù)下去,所以這兩個(gè)任務(wù)都不會(huì)結(jié)束自己的執(zhí)行過(guò)程。
資源不足
當(dāng)某個(gè)任務(wù)在系統(tǒng)中無(wú)法獲取維持其繼續(xù)執(zhí)行所需的資源時(shí),就會(huì)出現(xiàn)資源不足。當(dāng)有多個(gè)任務(wù)在等待某一資源且該資源被釋放時(shí),系統(tǒng)需要選擇下一個(gè)可以使用該資源的任務(wù)。如果你的系統(tǒng)中沒(méi)有設(shè)計(jì)良好的算法,那么系統(tǒng)中有些線程很可能要為獲取該資源而等待很長(zhǎng)時(shí)間。
要解決這一問(wèn)題就要確保公平原則。所有等待某一資源的任務(wù)必須在某一給定時(shí)間之內(nèi)占有該資源。可選方案之一就是實(shí)現(xiàn)一個(gè)算法,在選擇下一個(gè)將占有某一資源的任務(wù)時(shí),對(duì)任務(wù)已等待該資源的時(shí)間因素加以考慮。然而,實(shí)現(xiàn)鎖的公平需要增加額外的開(kāi)銷,這可能會(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)線程安全的問(wèn)題一般是因?yàn)?strong>主內(nèi)存和工作內(nèi)存數(shù)據(jù)不一致性和重排序導(dǎo)致的,而解決線程安全的問(wèn)題最重要的就是理解這兩種問(wèn)題是怎么來(lái)的,那么,理解它們的核心在于理解java內(nèi)存模型(JMM)。
Java 的并發(fā)采用的是共享內(nèi)存模型,Java 線程之間的通信總是隱式進(jìn)行,整個(gè)通信過(guò)程對(duì)程序員完全透明。如果編寫多線程程序的 Java 程序員不理解隱式進(jìn)行的線程之間通信的工作機(jī)制,很可能會(huì)遇到各種奇怪的內(nèi)存可見(jiàn)性問(wèn)題。我們需要處理兩個(gè)關(guān)鍵問(wèn)題:線程之間如何通信及線程之間如何同步(這里的線程是指并發(fā)執(zhí)行的活動(dòng)實(shí)體)。通信是指線程之間以何種機(jī)制來(lái)交換信息。緊接著我們需要知道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)存可見(jiàn)性問(wèn)題,也不受內(nèi)存模型的影響。
JMM內(nèi)存模型抽象
Java 線程之間的通信由 Java 內(nèi)存模型(JMM java method model)控制,JMM 決定一個(gè)線程對(duì)共享變量的寫入何時(shí)對(duì)另一個(gè)線程可見(jiàn)。從抽象的角度來(lái)看,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)存模型的抽象示意圖如下:
從上圖來(lái)看,線程 A 與線程 B 之間如要通信的話,必須要經(jīng)歷下面 2 個(gè)步驟:
- 首先,線程 A 把本地內(nèi)存 A 中更新過(guò)的共享變量刷新到主內(nèi)存中去。
- 然后,線程 B 到主內(nèi)存中去讀取線程 A 之前已更新過(guò)的共享變量。
線程A和線程B通過(guò)共享變量在進(jìn)行隱式通信。如果線程A更新后數(shù)據(jù)并沒(méi)有及時(shí)寫回到主存,而此時(shí)線程B讀到的是過(guò)期的數(shù)據(jù),這就出現(xiàn)了“臟讀”現(xiàn)象。可以通過(guò)同步機(jī)制(控制不同線程間操作發(fā)生的相對(duì)順序)來(lái)解決或者通過(guò)volatile關(guān)鍵字使得每次volatile變量都能夠強(qiáng)制刷新到主存,從而對(duì)每個(gè)線程都是可見(jiàn)的。
重排序(重要)
一個(gè)好的內(nèi)存模型實(shí)際上會(huì)放松對(duì)處理器和編譯器規(guī)則的束縛,也就是說(shuō)軟件技術(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的指令重排序。
編譯器重排序。
對(duì)于沒(méi)有先后依賴關(guān)系的語(yǔ)句,編譯器可以重新調(diào)整語(yǔ)句的執(zhí)行順序。
CPU指令重排序。
在指令級(jí)別,讓沒(méi)有依賴關(guān)系的多條指令并行。
CPU內(nèi)存重排序。
CPU有自己的緩存,指令的執(zhí)行順序和寫入主內(nèi)存的順序不完全一致。
**1屬于編譯器重排序,而2和3統(tǒng)稱為CPU處理器重排序。**這些重排序會(huì)導(dǎo)致線程安全的問(wèn)題,一個(gè)很經(jīng)典的例子就是DCL問(wèn)題.
假設(shè):X、Y是兩個(gè)全局變量,初始的時(shí)候,X,Y是全局變量并 X=0,Y=0。 線程A,B 分別執(zhí)行各自的值。線程1和線程2的執(zhí)行先后順序是不確定的,可能順序執(zhí)行,也可能交叉執(zhí)行,這就造成內(nèi)存可見(jiàn)性問(wèn)題。可能會(huì)出現(xiàn)結(jié)果可能是:
對(duì)于編譯器,JMM 的編譯器重排序規(guī)則會(huì)禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。對(duì)于CPU處理器重排序,JMM 的處理器重排序規(guī)則會(huì)要求 java 編譯器在生成指令序列時(shí),插入特定類型的內(nèi)存屏障指令,通過(guò)內(nèi)存屏障指令來(lá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提供的指令,可以由開(kāi)發(fā)者顯示調(diào)用。內(nèi)存屏障是很底層的概念,對(duì)于 Java 開(kāi)發(fā)者來(lái)說(shuō),一般用 volatile 關(guān)鍵字就足夠了。但從JDK 8開(kāi)始,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)存屏障分成四種:
LoadLoad:禁止讀和讀的重排序。
StoreStore:禁止寫和寫的重排序。
LoadStore:禁止讀和寫的重排序。
StoreLoad:禁止寫和讀的重排序。
Unsafe中的方法:
loadFence=LoadLoad+LoadStore
storeFence=StoreStore+LoadStore
fullFence=loadFence+storeFence+StoreLoad
as-if-serial語(yǔ)義(了解)
as-if-serial語(yǔ)義的意思是:不管怎么重排序(編譯器和處理器為了提供并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變。
重排序的原則是什么?什么場(chǎng)景下可以重排序,什么場(chǎng)景下不能重排序呢?
無(wú)論什么語(yǔ)言,站在編譯器和CPU的角度來(lái)說(shuō),不管怎么重排序,單線程程序的執(zhí)行結(jié)果不能改變,這就是單線程程序的重排序規(guī)則。
即只要操作之間沒(méi)有數(shù)據(jù)依賴性,編譯器和CPU都可以任意重排序,因?yàn)閳?zhí)行結(jié)果不會(huì)改變,代碼看起來(lái)就像是完全串行地一行行從頭執(zhí)行到尾,這也就是as-if-serial語(yǔ)義。
對(duì)于單線程程序來(lái)說(shuō),編譯器和CPU可能做了重排序,但開(kāi)發(fā)者感知不到,也不存在內(nèi)存可見(jiàn)性問(wèn)題。
編譯器和CPU的這一行為對(duì)于單線程程序沒(méi)有影響,但對(duì)多線程程序卻有影響。
對(duì)于多線程程序來(lái)說(shuō),線程之間的數(shù)據(jù)依賴性太復(fù)雜,編譯器和CPU沒(méi)有辦法完全理解這種依賴性并據(jù)此做出最合理的優(yōu)化。
編譯器和CPU只能保證每個(gè)線程的as-if-serial語(yǔ)義。
線程之間的數(shù)據(jù)依賴和相互影響,需要編譯器和CPU的上層來(lái)確定。
上層要告知編譯器和CPU在多線程場(chǎng)景下什么時(shí)候可以重排序,什么時(shí)候不能重排序。
happens-before定義
JMM可以通過(guò)happens-before關(guān)系向程序員提供跨線程的內(nèi)存可見(jiàn)性保證這兩個(gè)操作既可以是在一個(gè)線程之內(nèi),也可以是在不同線程之間。
-
如果一個(gè)操作happens-before另一個(gè)操作,那么第一個(gè)操作的執(zhí)行結(jié)果將對(duì)第二個(gè)操作可見(jiàn),而且第一個(gè)操作的執(zhí)行順序排在第二個(gè)操作之前。
-
兩個(gè)操作之間存在happens-before關(guān)系,并不意味著Java平臺(tái)的具體實(shí)現(xiàn)必須要按照happens-before關(guān)系指定的順序來(lái)執(zhí)行。如果重排序之后的執(zhí)行結(jié)果,與按happens-before關(guān)系來(lái)執(zhí)行的結(jié)果一致,那么這種重排序并不非法(也就是說(shuō),JMM允許這種重排序)。
上面的1)是JMM對(duì)程序員的承諾。從程序員的角度來(lái)說(shuō),可以這樣理解happens-before關(guān)系:如果A happens-before B,那么Java內(nèi)存模型將向程序員保證——A操作的結(jié)果將對(duì)B可見(jiàn),且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ū)別
happens-before規(guī)則(了解)
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í)可能有問(wèn)題。那豈不是說(shuō),在多線程程序中,我們要么加鎖,要么必須把所有變量都聲明為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ǔ)義。展開(kāi)上面的代碼可得到類似于下面的偽代碼:
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ī)則。
- 上層會(huì)有基于JMM的關(guān)鍵字和J.U.C包下的一些具體類用來(lái)方便程序員能夠迅速高效率的進(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)存模型來(lái)編寫代碼
- 編譯器和處理器對(duì)內(nèi)存模型的實(shí)現(xiàn) 編譯器和處理器希望內(nèi)存模型對(duì)它們的束縛越少越好,這樣它們就可以做盡可能多的優(yōu)化來(lái)提高性能。編譯器和處理器希望實(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 允許這種重排序)
從上圖可以看出兩點(diǎn):
- JMM 向程序員提供的 happens- before 規(guī)則能滿足程序員的需求。JMM 的 happens- before 規(guī)則不但簡(jiǎn)單易懂,而且也向程序員提供了足夠強(qiáng)的內(nèi)存可見(jiàn)性保證(有些內(nèi)存可見(jiàn)性保證其實(shí)并不一定真實(shí)存在,比如上面的 A happens- before B)。
- JMM 對(duì)編譯器和處理器的束縛已經(jīng)盡可能的少。從上面的分析我們可以看出,JMM 其實(shí)是在遵循一個(gè)基本原則:只要不改變程序的執(zhí)行結(jié)果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都行。比如,如果編譯器經(jīng)過(guò)細(xì)致的分析后,認(rèn)定一個(gè)鎖只會(huì)被單個(gè)線程訪問(wèn),那么這個(gè)鎖可以被消除。再比如,如果編譯器經(jīng)過(guò)細(xì)致的分析后,認(rèn)定一個(gè) volatile 變量?jī)H僅只會(huì)被單個(gè)線程訪問(wèn),那么編譯器可以把這個(gè) volatile 變量當(dāng)作一個(gè)普通變量來(lái)對(duì)待。這些優(yōu)化既不會(huì)改變程序的執(zhí)行結(jié)果,又能提高程序的執(zhí)行效率。
JMM 的內(nèi)存可見(jiàn)性保證(重要)
Java 程序的內(nèi)存可見(jiàn)性保證按程序類型可以分為下列三類:
- 單線程程序。單線程程序不會(huì)出現(xiàn)內(nèi)存可見(jiàn)性問(wèn)題。編譯器,runtime 和處理器會(huì)共同確保單線程程序的執(zhí)行結(jié)果與該程序在順序一致性模型中的執(zhí)行結(jié)果相同。
- 正確同步的多線程程序。正確同步的多線程程序的執(zhí)行將具有順序一致性(程序的執(zhí)行結(jié)果與該程序在順序一致性內(nèi)存模型中的執(zhí)行結(jié)果相同)。這是 JMM 關(guān)注的重點(diǎn),JMM 通過(guò)限制編譯器和處理器的重排序來(lái)為程序員提供內(nèi)存可見(jiàn)性保證。
- 未同步 / 未正確同步的多線程程序。JMM 為它們提供了最小安全性保障:線程執(zhí)行時(shí)讀取到的值,要么是之前某個(gè)線程寫入的值,要么是默認(rèn)值(0,null,false)。
下圖展示了這三類程序在 JMM 中與在順序一致性內(nèi)存模型中的執(zhí)行結(jié)果的異同:
標(biāo)注:在學(xué)習(xí)中需要修改的內(nèi)容以及筆記全在這里 www.javanode.cn,謝謝!有任何不妥的地方望糾正
總結(jié)
以上是生活随笔為你收集整理的最新详细的JMM内存模型(三天熬夜血肝)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 脱发篇-多线程基础(下)来看看你知道多少
- 下一篇: 1202年最新最详细最全的synchro