啃碎并發(fā)(九):內(nèi)存模型之基礎(chǔ)概述

前言

在并發(fā)編程中调炬,需要解決兩個(gè)關(guān)鍵問題:

線程之間如何通信函筋;

線程之間如何同步盼忌;


線程通信是指線程之間以何種機(jī)制來交換信息贝润。在命令式編程中绊茧,線程之間的通信機(jī)制有兩種:共享內(nèi)存和消息傳遞

在共享內(nèi)存的并發(fā)模型里打掘,線程之間共享程序的公共狀態(tài)华畏,線程之間通過寫-讀內(nèi)存中的公共狀態(tài)來隱式進(jìn)行通信。

在消息傳遞的并發(fā)模型里尊蚁,線程之間沒有公共狀態(tài)亡笑,線程之間必須通過明確的發(fā)送消息來顯式進(jìn)行通信。


線程同步是指程序用于控制不同線程之間操作發(fā)生相對順序的機(jī)制横朋。

在共享內(nèi)存的并發(fā)模型里仑乌,同步是顯式進(jìn)行的。程序員必須顯式指定某個(gè)方法或某段代碼需要在線程之間互斥執(zhí)行琴锭。

在消息傳遞的并發(fā)模型里绝骚,由于消息的發(fā)送必須在消息的接收之前,因此同步是隱式進(jìn)行的祠够。


Java的并發(fā)采用的是共享內(nèi)存模型压汪,Java線程之間的通信總是隱式進(jìn)行,整個(gè)通信過程對程序員完全透明 — 隱式通信古瓤、顯示同步止剖。如果你想設(shè)計(jì)表現(xiàn)良好的并發(fā)程序,理解Java內(nèi)存模型是非常重要的落君。Java內(nèi)存模型規(guī)定了如何和何時(shí)可以看到由其他線程修改過后的共享變量的值穿香,以及在必須時(shí)如何同步的訪問共享變量。


1 為什么要有內(nèi)存模型

在介紹Java內(nèi)存模型之前绎速,先來看一下到底什么是計(jì)算機(jī)內(nèi)存模型皮获,然后再來看Java內(nèi)存模型在計(jì)算機(jī)內(nèi)存模型的基礎(chǔ)上做了哪些事情。要說計(jì)算機(jī)的內(nèi)存模型纹冤,就要說一下一段古老的歷史洒宝,看一下為什么要有內(nèi)存模型。


1.1 CPU和緩存一致性

我們應(yīng)該都知道萌京,計(jì)算機(jī)在執(zhí)行程序的時(shí)候雁歌,每條指令都是在CPU中執(zhí)行的,而執(zhí)行的時(shí)候知残,又免不了要和數(shù)據(jù)打交道靠瞎。而計(jì)算機(jī)上面的數(shù)據(jù),是存放在主存當(dāng)中的,也就是計(jì)算機(jī)的物理內(nèi)存啦乏盐。

剛開始佳窑,還相安無事的,但是隨著CPU技術(shù)的發(fā)展父能,CPU的執(zhí)行速度越來越快华嘹。而由于內(nèi)存的技術(shù)并沒有太大的變化,所以從內(nèi)存中讀取和寫入數(shù)據(jù)的過程和CPU的執(zhí)行速度比起來差距就會(huì)越來越大法竞,這就導(dǎo)致CPU每次操作內(nèi)存都要耗費(fèi)很多等待時(shí)間

這就像一家創(chuàng)業(yè)公司强挫,剛開始岔霸,創(chuàng)始人和員工之間工作關(guān)系其樂融融,但是隨著創(chuàng)始人的能力和野心越來越大俯渤,逐漸和員工之間出現(xiàn)了差距呆细,普通員工原來越跟不上CEO的腳步。老板的每一個(gè)命令八匠,傳到到基層員工之后絮爷,由于基層員工的理解能力、執(zhí)行能力的欠缺梨树,就會(huì)耗費(fèi)很多時(shí)間坑夯。這也就無形中拖慢了整家公司的工作效率。

可是抡四,不能因?yàn)閮?nèi)存的讀寫速度慢柜蜈,就不發(fā)展CPU技術(shù)了吧,總不能讓內(nèi)存成為計(jì)算機(jī)處理的瓶頸吧指巡。

所以淑履,人們想出來了一個(gè)好的辦法,就是在CPU和內(nèi)存之間增加高速緩存藻雪。緩存的概念大家都知道秘噪,就是保存一份數(shù)據(jù)拷貝。它的特點(diǎn)是速度快勉耀,內(nèi)存小指煎,并且昂貴

那么便斥,程序的執(zhí)行過程就變成了:當(dāng)程序在運(yùn)行過程中贯要,會(huì)將運(yùn)算需要的數(shù)據(jù)從主存復(fù)制一份到CPU的高速緩存當(dāng)中,那么CPU進(jìn)行計(jì)算時(shí)就可以直接從它的高速緩存讀取數(shù)據(jù)和向其中寫入數(shù)據(jù)椭住,當(dāng)運(yùn)算結(jié)束之后崇渗,再將高速緩存中的數(shù)據(jù)刷新到主存當(dāng)中

之后,這家公司開始設(shè)立中層管理人員宅广,管理人員直接歸CEO領(lǐng)導(dǎo)葫掉,領(lǐng)導(dǎo)有什么指示,直接告訴管理人員跟狱,然后就可以去做自己的事情了俭厚。管理人員負(fù)責(zé)去協(xié)調(diào)底層員工的工作。因?yàn)楣芾砣藛T是了解手下的人員以及自己負(fù)責(zé)的事情的驶臊。所以挪挤,大多數(shù)時(shí)候,公司的各種決策关翎,通知等扛门,CEO只要和管理人員之間溝通就夠了。

而隨著CPU能力的不斷提升纵寝,一層緩存就慢慢的無法滿足要求了论寨,就逐漸的衍生出多級緩存。

按照數(shù)據(jù)讀取順序和與CPU結(jié)合的緊密程度爽茴,CPU緩存可以分為一級緩存(L1)葬凳,二級緩存(L3),部分高端CPU還具有三級緩存(L3)室奏,每一級緩存中所儲存的全部數(shù)據(jù)都是下一級緩存的一部分火焰。這三種緩存的技術(shù)難度和制造成本是相對遞減的,所以其容量也是相對遞增的胧沫。

那么荐健,在有了多級緩存之后,程序的執(zhí)行就變成了:當(dāng)CPU要讀取一個(gè)數(shù)據(jù)時(shí)琳袄,首先從一級緩存中查找江场,如果沒有找到再從二級緩存中查找,如果還是沒有就從三級緩存或內(nèi)存中查找窖逗。

隨著公司越來越大址否,老板要管的事情越來越多,公司的管理部門開始改革碎紊,開始出現(xiàn)高層佑附,中層,底層等管理者仗考。一級一級之間逐層管理音同。

單核CPU只含有一套L1,L2秃嗜,L3緩存权均。如果CPU含有多個(gè)核心顿膨,即多核CPU,則每個(gè)核心都含有一套L1(甚至和L2)緩存叽赊,而共享L3(或者和L2)緩存恋沃。

公司也分很多種,有些公司只有一個(gè)大Boss必指,他一個(gè)人說了算囊咏。但是有些公司有比如聯(lián)席總經(jīng)理、合伙人等機(jī)制塔橡。

單核CPU就像一家公司只有一個(gè)老板梅割,所有命令都來自于他,那么就只需要一套管理班底就夠了葛家。

多核CPU就像一家公司是由多個(gè)合伙人共同創(chuàng)辦的户辞,那么,就需要給每個(gè)合伙人都設(shè)立一套供自己直接領(lǐng)導(dǎo)的高層管理人員惦银,多個(gè)合伙人共享使用的是公司的底層員工。

還有的公司末誓,不斷壯大扯俱,開始差分出各個(gè)子公司。各個(gè)子公司就是多個(gè)CPU了喇澡,互相之前沒有共用的資源迅栅。互不影響晴玖。



一個(gè)單CPU雙核的緩存結(jié)構(gòu)


隨著計(jì)算機(jī)能力不斷提升读存,開始支持多線程。那么問題就來了呕屎。我們分別來分析下單線程让簿、多線程在單核CPU、多核CPU中的影響秀睛。

單線程:CPU核心的緩存只被一個(gè)線程訪問尔当。緩存獨(dú)占,不會(huì)出現(xiàn)訪問沖突等問題蹂安。

單核CPU椭迎,多線程:進(jìn)程中的多個(gè)線程會(huì)同時(shí)訪問進(jìn)程中的共享數(shù)據(jù),CPU將某塊內(nèi)存加載到緩存后田盈,不同線程在訪問相同的物理地址的時(shí)候畜号,都會(huì)映射到相同的緩存位置,這樣即使發(fā)生線程的切換允瞧,緩存仍然不會(huì)失效简软。但由于任何時(shí)刻只能有一個(gè)線程在執(zhí)行蛮拔,因此不會(huì)出現(xiàn)緩存訪問沖突。

多核CPU替饿,多線程:每個(gè)核都至少有一個(gè)L1 緩存语泽。多個(gè)線程訪問進(jìn)程中的某個(gè)共享內(nèi)存,且這多個(gè)線程分別在不同的核心上執(zhí)行视卢,則每個(gè)核心都會(huì)在各自的caehe中保留一份共享內(nèi)存的緩沖踱卵。由于多核是可以并行的,可能會(huì)出現(xiàn)多個(gè)線程同時(shí)寫各自的緩存的情況据过,而各自的cache之間的數(shù)據(jù)就有可能不同惋砂。

在CPU和主存之間增加緩存,在多線程場景下就可能存在緩存一致性問題绳锅,也就是說西饵,在多核CPU中,每個(gè)核的自己的緩存中鳞芙,關(guān)于同一個(gè)數(shù)據(jù)的緩存內(nèi)容可能不一致眷柔。

如果這家公司的命令都是串行下發(fā)的話,那么就沒有任何問題原朝。

如果這家公司的命令都是并行下發(fā)的話驯嘱,并且這些命令都是由同一個(gè)CEO下發(fā)的,這種機(jī)制是也沒有什么問題喳坠。因?yàn)樗拿顖?zhí)行者只有一套管理體系鞠评。

如果這家公司的命令都是并行下發(fā)的話,并且這些命令是由多個(gè)合伙人下發(fā)的壕鹉,這就有問題了剃幌。因?yàn)槊總€(gè)合伙人只會(huì)把命令下達(dá)給自己直屬的管理人員,而多個(gè)管理人員管理的底層員工可能是公用的晾浴。

比如负乡,合伙人1要辭退員工a,合伙人2要給員工a升職脊凰,升職后的話他再被辭退需要多個(gè)合伙人開會(huì)決議敬鬓。兩個(gè)合伙人分別把命令下發(fā)給了自己的管理人員。合伙人1命令下達(dá)后笙各,管理人員a在辭退了員工后钉答,他就知道這個(gè)員工被開除了。而合伙人2的管理人員2這時(shí)候在沒得到消息之前杈抢,還認(rèn)為員工a是在職的数尿,他就欣然的接收了合伙人給他的升職a的命令。



多核CPU多線程場景下緩存不一致問題



1.2 處理器優(yōu)化和指令重排

上面提到在CPU和主存之間增加緩存惶楼,在多線程場景下會(huì)存在緩存一致性問題右蹦。除了這種情況诊杆,還有一種硬件問題也比較重要。那就是為了使處理器內(nèi)部的運(yùn)算單元能夠盡量的被充分利用何陆,處理器可能會(huì)對輸入代碼進(jìn)行亂序執(zhí)行處理晨汹。這就是處理器優(yōu)化

除了現(xiàn)在很多流行的處理器會(huì)對代碼進(jìn)行優(yōu)化亂序處理贷盲,很多編程語言的編譯器也會(huì)有類似的優(yōu)化淘这,比如:Java虛擬機(jī)的即時(shí)編譯器(JIT)也會(huì)做指令重排

可想而知巩剖,如果任由處理器優(yōu)化和編譯器對指令重排的話铝穷,就可能導(dǎo)致各種各樣的問題

關(guān)于員工組織調(diào)整的情況佳魔,如果允許人事部在接到多個(gè)命令后進(jìn)行隨意拆分亂序執(zhí)行或者重排的話曙聂,那么對于這個(gè)員工以及這家公司的影響是非常大的。


1.3 并發(fā)編程的問題

前面說的和硬件有關(guān)的概念你可能聽得有點(diǎn)蒙鞠鲜,還不知道他到底和軟件有啥關(guān)系宁脊。但是關(guān)于并發(fā)編程的問題你應(yīng)該有所了解,比如:原子性問題贤姆,可見性問題和有序性問題榆苞。

其實(shí),原子性問題庐氮,可見性問題和有序性問題语稠,是人們抽象定義出來的宋彼。而這個(gè)抽象的底層問題就是前面提到的緩存一致性問題弄砍、處理器優(yōu)化問題和指令重排問題等。緩存一致性問題其實(shí)就是可見性問題输涕。而處理器優(yōu)化是可以導(dǎo)致原子性問題的音婶。指令重排即會(huì)導(dǎo)致有序性問題

原子性是指在一個(gè)操作中就是CPU不可以在中途暫停然后再調(diào)度莱坎,既不被中斷操作衣式,要不執(zhí)行完成,要不就不執(zhí)行檐什。

可見性是指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí)碴卧,一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值乃正。

有序性是指程序執(zhí)行的順序按照代碼的先后順序執(zhí)行住册。


2 什么是內(nèi)存模型

前面提到的,緩存一致性問題瓮具、處理器器優(yōu)化的指令重排問題是硬件的不斷升級導(dǎo)致的荧飞。那么凡人,有沒有什么機(jī)制可以很好的解決上面的這些問題呢?

最簡單直接的做法就是廢除處理器和處理器的優(yōu)化技術(shù)叹阔、廢除CPU緩存挠轴,讓CPU直接和主存交互。但是耳幢,這么做雖然可以保證多線程下的并發(fā)問題岸晦。但是,這就有點(diǎn)因噎廢食了帅掘。

所以委煤,為了保證并發(fā)編程中可以滿足原子性、可見性及有序性修档。有一個(gè)重要的概念碧绞,那就是——內(nèi)存模型,定義了共享內(nèi)存系統(tǒng)中多線程程序讀寫操作行為的規(guī)范吱窝。

通過這些規(guī)則來規(guī)范對內(nèi)存的讀寫操作讥邻,從而保證指令執(zhí)行的正確性。它與處理器有關(guān)院峡、與緩存有關(guān)兴使、與并發(fā)有關(guān)、與編譯器也有關(guān)照激。它解決了CPU多級緩存发魄、處理器優(yōu)化、指令重排等導(dǎo)致的內(nèi)存訪問問題俩垃,保證了并發(fā)場景下的一致性励幼、原子性和有序性。

內(nèi)存模型解決并發(fā)問題主要采用兩種方式:限制處理器優(yōu)化和使用內(nèi)存屏障口柳。


3 什么是Java內(nèi)存模型

前面介紹過了計(jì)算機(jī)內(nèi)存模型苹粟,這是解決多線程場景下并發(fā)問題的一個(gè)重要規(guī)范。那么具體的實(shí)現(xiàn)是如何的呢跃闹,不同的編程語言嵌削,在實(shí)現(xiàn)上可能有所不同

我們知道望艺,Java程序是需要運(yùn)行在Java虛擬機(jī)上面的苛秕,Java內(nèi)存模型(Java Memory Model ,JMM)就是一種符合內(nèi)存模型規(guī)范的,屏蔽了各種硬件和操作系統(tǒng)的訪問差異的找默,保證了Java程序在各種平臺下對內(nèi)存的訪問都能保證效果一致的機(jī)制及規(guī)范艇劫。

Java內(nèi)存模型規(guī)定了所有的變量都存儲在主內(nèi)存中,每條線程還有自己的工作內(nèi)存啡莉,線程的工作內(nèi)存中保存了該線程中用到的變量的主內(nèi)存副本拷貝港准,線程對變量的所有操作都必須在工作內(nèi)存中進(jìn)行旨剥,而不能直接讀寫主內(nèi)存。不同的線程之間也無法直接訪問對方工作內(nèi)存中的變量浅缸,線程間變量的傳遞均需要自己的工作內(nèi)存和主存之間進(jìn)行數(shù)據(jù)同步進(jìn)行轨帜。

而JMM就作用于工作內(nèi)存和主存之間數(shù)據(jù)同步過程。他規(guī)定了如何做數(shù)據(jù)同步以及什么時(shí)候做數(shù)據(jù)同步衩椒。



主內(nèi)存與工作內(nèi)存交互示意


特別需要注意的是蚌父,主內(nèi)存和工作內(nèi)存與JVM內(nèi)存結(jié)構(gòu)中的Java堆、棧毛萌、方法區(qū)等并不是同一個(gè)層次的內(nèi)存劃分苟弛,無法直接類比

再來總結(jié)下阁将,JMM是一種規(guī)范膏秫,規(guī)范了Java虛擬機(jī)與計(jì)算機(jī)內(nèi)存是如何協(xié)同工作的,目的是解決由于多線程通過共享內(nèi)存進(jìn)行通信時(shí)做盅,存在的本地內(nèi)存數(shù)據(jù)不一致缤削、編譯器會(huì)對代碼指令重排序、處理器會(huì)對代碼亂序執(zhí)行等帶來的問題吹榴。目的是保證并發(fā)編程場景中的原子性亭敢、可見性和有序性。

所以图筹,如果你想設(shè)計(jì)表現(xiàn)良好的并發(fā)程序帅刀,理解Java內(nèi)存模型是非常重要的。Java內(nèi)存模型規(guī)定了如何和何時(shí)可以看到由其他線程修改過后的共享變量的值远剩,以及在必須時(shí)如何同步的訪問共享變量扣溺。

3.1 Java內(nèi)存模型抽象

在Java中,所有實(shí)例域民宿、靜態(tài)域和數(shù)組元素存儲在堆內(nèi)存中娇妓,堆內(nèi)存在線程之間共享像鸡。局部變量(Local variables)活鹰,方法定義參數(shù)(formal method parameters)和異常處理器參數(shù)(exception handler parameters)不會(huì)在線程之間共享,它們不會(huì)有內(nèi)存可見性問題只估,也不受內(nèi)存模型的影響志群。

Java線程之間的通信由Java內(nèi)存模型(JMM)控制,JMM決定一個(gè)線程對共享變量的寫入何時(shí)對另一個(gè)線程可見蛔钙。從抽象的角度來看锌云,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:

線程之間的共享變量存儲在主內(nèi)存(main memory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(local memory)吁脱,本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本桑涎。本地內(nèi)存是JMM的一個(gè)抽象概念彬向,并不真實(shí)存在。它涵蓋了緩存攻冷,寫緩沖區(qū)娃胆,寄存器以及其他的硬件和編譯器優(yōu)化

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


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

首先,線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去;

然后,線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量疼约;

下面通過示意圖來說明這兩個(gè)步驟:


線程A與線程B之間通信


如上圖所示寸五,本地內(nèi)存A和B有主內(nèi)存中共享變量x的副本。假設(shè)初始時(shí)乾蛤,這三個(gè)內(nèi)存中的x值都為0。線程A在執(zhí)行時(shí),把更新后的x值(假設(shè)值為1)臨時(shí)存放在自己的本地內(nèi)存A中触趴。當(dāng)線程A和線程B需要通信時(shí),線程A首先會(huì)把自己本地內(nèi)存中修改后的x值刷新到主內(nèi)存中渴肉,此時(shí)主內(nèi)存中的x值變?yōu)榱?冗懦。隨后,線程B到主內(nèi)存中去讀取線程A更新后的x值仇祭,此時(shí)線程B的本地內(nèi)存的x值也變?yōu)榱?披蕉。

從整體來看,這兩個(gè)步驟實(shí)質(zhì)上是線程A在向線程B發(fā)送消息乌奇,而且這個(gè)通信過程必須要經(jīng)過主內(nèi)存没讲。JMM通過控制主內(nèi)存與每個(gè)線程的本地內(nèi)存之間的交互,來為Java程序員提供內(nèi)存可見性保證礁苗。

3.2 重排序

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

1.編譯器優(yōu)化的重排序试伙。編譯器在不改變單線程程序語義的前提下嘁信,可以重新安排語句的執(zhí)行順序。

2.指令級并行的重排序∈柽叮現(xiàn)代處理器采用了指令級并行技術(shù)(Instruction-Level Parallelism潘靖, ILP)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性蚤蔓,處理器可以改變語句對應(yīng)機(jī)器指令的執(zhí)行順序卦溢。

3.內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲操作看上去可能是在亂序執(zhí)行单寂。


從Java源代碼到最終實(shí)際執(zhí)行的指令序列贬芥,會(huì)分別經(jīng)歷下面三種重排序:

三種重排序


上述的1屬于編譯器重排序,2和3屬于處理器重排序宣决。這些重排序都可能會(huì)導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見性問題誓军。

對于編譯器,JMM的編譯器重排序規(guī)則會(huì)禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)疲扎。

對于處理器重排序昵时,JMM的處理器重排序規(guī)則會(huì)要求Java編譯器在生成指令序列時(shí),插入特定類型的內(nèi)存屏障(memory barriers椒丧,intel稱之為memory fence)指令壹甥,通過內(nèi)存屏障指令來禁止特定類型的處理器重排序(不是所有的處理器重排序都要禁止)

JMM屬于語言級的內(nèi)存模型壶熏,它確保在不同的編譯器和不同的處理器平臺之上句柠,通過禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內(nèi)存可見性保證棒假。

3.3 處理器重排序

現(xiàn)代的處理器使用寫緩沖區(qū)來臨時(shí)保存向內(nèi)存寫入的數(shù)據(jù)溯职。寫緩沖區(qū)可以保證指令流水線持續(xù)運(yùn)行,它可以避免由于處理器停頓下來等待向內(nèi)存寫入數(shù)據(jù)而產(chǎn)生的延遲帽哑。同時(shí)谜酒,通過以批處理的方式刷新寫緩沖區(qū),以及合并寫緩沖區(qū)中對同一內(nèi)存地址的多次寫妻枕,可以減少對內(nèi)存總線的占用僻族。雖然寫緩沖區(qū)有這么多好處,但每個(gè)處理器上的寫緩沖區(qū)屡谐,僅僅對它所在的處理器可見述么。這個(gè)特性會(huì)對內(nèi)存操作的執(zhí)行順序產(chǎn)生重要的影響:

處理器對內(nèi)存的讀/寫操作的執(zhí)行順序,不一定與內(nèi)存實(shí)際發(fā)生的讀/寫操作順序一致愕掏!


兩個(gè)處理器示例


假設(shè)處理器A和處理器B按程序的順序并行執(zhí)行內(nèi)存訪問度秘,最終卻可能得到 x = y = 0。具體的原因如下圖所示


處理器A和處理器B并行執(zhí)行程序


處理器 A 和 B 同時(shí)把共享變量寫入在寫緩沖區(qū)中(A1饵撑、B1)剑梳,然后再從內(nèi)存中讀取另一個(gè)共享變量(A2、B2)肄梨,最后才把自己寫緩沖區(qū)中保存的臟數(shù)據(jù)刷新到內(nèi)存中(A3阻荒、B3)挠锥。當(dāng)以這種時(shí)序執(zhí)行時(shí)众羡,程序就可以得到 x = y = 0 的結(jié)果

從內(nèi)存操作實(shí)際發(fā)生的順序來看蓖租,直到處理器 A 執(zhí)行 A3 來刷新自己的寫緩存區(qū)粱侣,寫操作 A1 才算真正執(zhí)行了羊壹。雖然處理器 A 執(zhí)行內(nèi)存操作的順序?yàn)椋篈1 -> A2,但內(nèi)存操作實(shí)際發(fā)生的順序卻是:A2 -> A1齐婴。此時(shí)油猫,處理器 A 的內(nèi)存操作順序被重排序了

這里的關(guān)鍵是柠偶,由于寫緩沖區(qū)僅對自己的處理器可見情妖,它會(huì)導(dǎo)致處理器執(zhí)行內(nèi)存操作的順序可能會(huì)與內(nèi)存實(shí)際的操作執(zhí)行順序不一致。由于現(xiàn)代的處理器都會(huì)使用寫緩沖區(qū)诱担,因此現(xiàn)代的處理器都會(huì)允許對內(nèi)存寫-讀操作重排序毡证。

3.4 內(nèi)存屏障指令

下面是常見處理器允許的重排序類型的列表:


常見處理器允許的重排序類型




上表單元格中的“N”表示處理器不允許兩個(gè)操作重排序,“Y”表示允許重排序蔫仙。從上表我們可以看出:常見的處理器都允許Store-Load重排序料睛;常見的處理器都不允許對存在數(shù)據(jù)依賴的操作做重排序。sparc-TSO和x86擁有相對較強(qiáng)的處理器內(nèi)存模型摇邦,它們僅允許對寫-讀操作做重排序(因?yàn)樗鼈兌际褂昧藢懢彌_區(qū))恤煞。

為了保證內(nèi)存可見性,Java 編譯器在生成指令序列的適當(dāng)位置會(huì)插入內(nèi)存屏障指令來禁止特定類型的處理器重排序施籍。JMM 把內(nèi)存屏障指令分為下列四類:


內(nèi)存屏障指令


3.5 happens-before

JSR-133 內(nèi)存模型使用happens-before的概念來闡述操作之間的內(nèi)存可見性居扒。在 JMM 中,如果一個(gè)操作執(zhí)行的結(jié)果需要對另一個(gè)操作可見丑慎,那么這兩個(gè)操作之間必須要存在 happens-before 關(guān)系苔货。這里提到的兩個(gè)操作既可以是在一個(gè)線程之內(nèi),也可以是在不同線程之間立哑。

與程序員密切相關(guān)的 happens-before 規(guī)則如下:

程序順序規(guī)則:一個(gè)線程中的每個(gè)操作夜惭,happens- before 于該線程中的任意后續(xù)操作。

監(jiān)視器鎖規(guī)則:對一個(gè)監(jiān)視器鎖的解鎖铛绰,happens- before 于隨后對這個(gè)監(jiān)視器鎖的加鎖诈茧。

volatile變量規(guī)則:對一個(gè)volatile域的寫,happens- before 于任意后續(xù)對這個(gè)volatile域的讀捂掰。

傳遞性規(guī)則:如果A happens- before B敢会,且B happens- before C,那么A happens- before C这嚣。

注意鸥昏,兩個(gè)操作之間具有happens-before關(guān)系,并不意味著前一個(gè)操作必須要在后一個(gè)操作之前執(zhí)行姐帚!happens-before僅僅要求前一個(gè)操作(執(zhí)行的結(jié)果)對后一個(gè)操作可見吏垮,且前一個(gè)操作按順序排在第二個(gè)操作之前(the first is visible to and ordered before the second)


happens-before與JMM的關(guān)系


如上圖所示,一個(gè)happens-before規(guī)則通常對應(yīng)于多個(gè)編譯器和處理器重排序規(guī)則膳汪。對于Java程序員來說唯蝶,happens-before規(guī)則簡單易懂,它避免java程序員為了理解JMM提供的內(nèi)存可見性保證而去學(xué)習(xí)復(fù)雜的重排序規(guī)則以及這些規(guī)則的具體實(shí)現(xiàn)遗嗽。

3.6 數(shù)據(jù)依賴性

如果兩個(gè)操作訪問同一個(gè)變量粘我,且這兩個(gè)操作中有一個(gè)為寫操作,此時(shí)這兩個(gè)操作之間就存在數(shù)據(jù)依賴性痹换。數(shù)據(jù)依賴分下列三種類型:


三種類型數(shù)據(jù)依賴



上面三種情況征字,只要重排序兩個(gè)操作的執(zhí)行順序,程序的執(zhí)行結(jié)果將會(huì)被改變娇豫。

前面提到過柔纵,編譯器和處理器可能會(huì)對操作做重排序。編譯器和處理器在重排序時(shí)锤躁,會(huì)遵守?cái)?shù)據(jù)依賴性搁料,編譯器和處理器不會(huì)改變存在數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的執(zhí)行順序

注意系羞,這里所說的數(shù)據(jù)依賴性僅針對單個(gè)處理器中執(zhí)行的指令序列和單個(gè)線程中執(zhí)行的操作郭计,不同處理器之間和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮

3.7 as-if-serial 語義

as-if-serial 語義的意思指:不管怎么重排序(編譯器和處理器為了提高并行度)椒振,(單線程)程序的執(zhí)行結(jié)果不能被改變昭伸。編譯器,runtime 和處理器都必須遵守 as-if-serial 語義澎迎。

為了遵守 as-if-serial 編譯器和處理器不會(huì)對存在數(shù)據(jù)依賴關(guān)系的操作做重排序庐杨,因?yàn)檫@種重排序會(huì)改變執(zhí)行結(jié)果。但是如果操作之間沒有數(shù)據(jù)依賴關(guān)系夹供,這些操作就可能被編譯器和處理器重排序灵份。

舉個(gè)例子:


上面三個(gè)操作的數(shù)據(jù)依賴關(guān)系如下圖所示:


三個(gè)操作的數(shù)據(jù)依賴關(guān)系


如上圖所示,A 和 C 之間存在數(shù)據(jù)依賴關(guān)系哮洽,同時(shí) B 和 C 之間也存在數(shù)據(jù)依賴關(guān)系填渠。因此在最終執(zhí)行的指令序列中,C 不能被重排序到 A 和 B 的前面(C 排到 A 和 B 的前面鸟辅,程序的結(jié)果將會(huì)被改變)氛什。但 A 和 B 之間沒有數(shù)據(jù)依賴關(guān)系,編譯器和處理器可以重排序 A 和 B 之間的執(zhí)行順序匪凉。下圖是該程序的兩種可能執(zhí)行順序:


兩種可能的執(zhí)行順序



在計(jì)算機(jī)中枪眉,軟件技術(shù)和硬件技術(shù)有一個(gè)共同的目標(biāo):在不改變程序執(zhí)行結(jié)果的前提下,盡可能的開發(fā)并行度再层。編譯器和處理器遵從這一目標(biāo)贸铜,從happens-before的定義我們可以看出堡纬,JMM 同樣遵從這一目標(biāo)。

4 Java內(nèi)存模型實(shí)現(xiàn)

了解Java多線程的朋友都知道萨脑,在Java中提供了一系列和并發(fā)處理相關(guān)的關(guān)鍵字隐轩,比如:volatile饺饭、synchronized渤早、finalconcurrent包等瘫俊。其實(shí)這些就是Java內(nèi)存模型封裝了底層的實(shí)現(xiàn)后提供給程序員使用的一些關(guān)鍵字鹊杖。

在開發(fā)多線程的代碼的時(shí)候,我們可以直接使用synchronized等關(guān)鍵字來控制并發(fā)扛芽,從來就不需要關(guān)心底層的編譯器優(yōu)化骂蓖、緩存一致性等問題。所以川尖,Java內(nèi)存模型登下,除了定義了一套規(guī)范,還提供了一系列原語叮喳,封裝了底層實(shí)現(xiàn)后被芳,供開發(fā)者直接使用

4.1 原子性

在Java中馍悟,為了保證原子性畔濒,提供了兩個(gè)高級的字節(jié)碼指令monitorentermonitorexit,這兩個(gè)字節(jié)碼锣咒,在Java中對應(yīng)的關(guān)鍵字就是synchronized侵状。

因此,在Java中可以使用synchronized來保證方法和代碼塊內(nèi)的操作是原子性的毅整。

4.2 可見性

Java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存趣兄,在變量讀取前從主內(nèi)存刷新變量值的這種依賴主內(nèi)存作為傳遞媒介的方式來實(shí)現(xiàn)的

Java中的volatile關(guān)鍵字提供了一個(gè)功能悼嫉,那就是被其修飾的變量在被修改后可以立即同步到主內(nèi)存诽俯,被其修飾的變量在每次是用之前都從主內(nèi)存刷新。因此承粤,可以使用volatile來保證多線程操作時(shí)變量的可見性暴区。

除了volatile,Java中的synchronizedfinal兩個(gè)關(guān)鍵字也可以實(shí)現(xiàn)可見性辛臊。只不過實(shí)現(xiàn)方式不同仙粱,這里不再展開了。

4.3 有序性

在Java中彻舰,可以使用synchronizedvolatile來保證多線程之間操作的有序性伐割。實(shí)現(xiàn)方式有所區(qū)別:

volatile關(guān)鍵字會(huì)禁止指令重排候味;

synchronized關(guān)鍵字保證同一時(shí)刻只允許一條線程操作;

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末隔心,一起剝皮案震驚了整個(gè)濱河市白群,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌硬霍,老刑警劉巖帜慢,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異唯卖,居然都是意外死亡粱玲,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門拜轨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抽减,“玉大人,你說我怎么就攤上這事橄碾÷殉粒” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵法牲,是天一觀的道長史汗。 經(jīng)常有香客問我,道長皆串,這世上最難降的妖魔是什么淹办? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮恶复,結(jié)果婚禮上怜森,老公的妹妹穿的比我還像新娘。我一直安慰自己谤牡,他們只是感情好副硅,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著翅萤,像睡著了一般恐疲。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上套么,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天培己,我揣著相機(jī)與錄音,去河邊找鬼胚泌。 笑死省咨,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的玷室。 我是一名探鬼主播零蓉,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼笤受,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了敌蜂?” 一聲冷哼從身側(cè)響起箩兽,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎章喉,沒想到半個(gè)月后汗贫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡囊陡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年芳绩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了掀亥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片撞反。...
    茶點(diǎn)故事閱讀 39,696評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖搪花,靈堂內(nèi)的尸體忽然破棺而出遏片,到底是詐尸還是另有隱情,我是刑警寧澤撮竿,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布吮便,位于F島的核電站,受9級特大地震影響幢踏,放射性物質(zhì)發(fā)生泄漏髓需。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一房蝉、第九天 我趴在偏房一處隱蔽的房頂上張望僚匆。 院中可真熱鬧,春花似錦搭幻、人聲如沸咧擂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽松申。三九已至,卻和暖如春俯逾,著一層夾襖步出監(jiān)牢的瞬間贸桶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工桌肴, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留皇筛,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓识脆,卻偏偏與公主長得像设联,于是被迫代替她去往敵國和親善已。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評論 2 353