新聞中心
前言

并發(fā)編程從操作系統(tǒng)底層工作整體認(rèn)識(shí)開(kāi)始
上一篇我們從操作系統(tǒng)底層工作的整體了解了并發(fā)編程在硬件以及操作系統(tǒng)層面的一些知識(shí),本篇我們繼續(xù)來(lái)學(xué)習(xí)JMM模型以及Volatile關(guān)鍵字的那些面試必問(wèn)的一些知識(shí)點(diǎn)。
什么是JMM模型?
Java 內(nèi)存模型(Java Memory Model 簡(jiǎn)稱JMM)是一種抽象的概念,并不真實(shí)存在,它描述的一組規(guī)則或規(guī)范,通過(guò)這組規(guī)范定義了程序中各個(gè)變量(包括實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素)的訪問(wèn)方式。JVM運(yùn)行程序的實(shí)體是線程,而每個(gè)線程創(chuàng)建時(shí) JVM 都會(huì)為其創(chuàng)建一個(gè)工作內(nèi)存(有些地方稱為??臻g),用于存儲(chǔ)線程私有的數(shù)據(jù),而Java 內(nèi)存模型中規(guī)定所有變量都存儲(chǔ)在主內(nèi)存,其主內(nèi)存是共享內(nèi)存區(qū)域,所有線程都可以訪問(wèn),但線程對(duì)變量的操作(讀取賦值等)必須在工作內(nèi)存中進(jìn)行,首先要將變量從主內(nèi)存考吧到增加的工作內(nèi)存空間,然后對(duì)變量進(jìn)行操作,操作完成后再將變量寫回主內(nèi)存,不能直接操作主內(nèi)存中的變量,工作內(nèi)存中存儲(chǔ)這主內(nèi)存中的變量副本拷貝,工作內(nèi)存是每個(gè)線程的私有數(shù)據(jù)區(qū)域,因此不同的線程間無(wú)法訪問(wèn)對(duì)方的工作內(nèi)存,線程間的通信(傳值)必須通過(guò)主內(nèi)存來(lái)完成。
JMM 不同于 JVM 內(nèi)存區(qū)域模式
JMM 與 JVM 內(nèi)存區(qū)域的劃分是不同的概念層次,更恰當(dāng)說(shuō) JMM 描述的是一組規(guī)則,通過(guò)這組規(guī)則控制各個(gè)變量在共享數(shù)據(jù)區(qū)域內(nèi)和私有數(shù)據(jù)區(qū)域的訪問(wèn)方式,JMM是圍繞原子性、有序性、可見(jiàn)性展開(kāi)。JMM 與 Java 內(nèi)存區(qū)域唯一相似點(diǎn),都存在共享數(shù)據(jù)區(qū)域和私有數(shù)據(jù)區(qū)域,在 JMM 中主內(nèi)存屬于共享數(shù)據(jù)區(qū)域,從某個(gè)程度上講應(yīng)該包括了堆和方法區(qū),而工作內(nèi)存數(shù)據(jù)線程私有數(shù)據(jù)區(qū)域,從某個(gè)程度上講則應(yīng)該包括程序計(jì)數(shù)器、虛擬機(jī)棧以及本地方法棧。
線程、工作內(nèi)存、主內(nèi)存工作交互圖(基于JMM規(guī)范),如下:
主內(nèi)存
主要存儲(chǔ)的是Java實(shí)例對(duì)象,所有線程創(chuàng)建的實(shí)例對(duì)象都存放在主內(nèi)存中,不管該實(shí)例對(duì)象是成員變量還是方法中的本地變量(也稱局部變量),當(dāng)然也包括了共享的類信息、常量、靜態(tài)變量。由于是共享數(shù)據(jù)區(qū)域,多個(gè)線程同一個(gè)變量進(jìn)行訪問(wèn)可能會(huì)發(fā)送線程安全問(wèn)題。
工作內(nèi)存
主要存儲(chǔ)當(dāng)前方法的所有本地變量信息(工作內(nèi)存中存儲(chǔ)著主內(nèi)存中的變量副本拷貝),每個(gè)線程只能訪問(wèn)自己的工作內(nèi)存,即線程中的本地變量對(duì)其他線程是不可見(jiàn)的,就算是兩個(gè)線程執(zhí)行的是同一段代碼,它們也會(huì)在各自的工作內(nèi)存中創(chuàng)建屬于當(dāng)前線程的本地變量,當(dāng)然也包括了字節(jié)碼行號(hào)指示器、相關(guān)Native方法的信息。注意由于工作內(nèi)存是每個(gè)線程的私有數(shù)據(jù),線程間無(wú)法相互訪問(wèn)工作內(nèi)存,因此存儲(chǔ)在工作內(nèi)存的數(shù)據(jù)不存在線程安全問(wèn)題。
根據(jù) JVM 虛擬機(jī)規(guī)范主內(nèi)存與工作內(nèi)存的數(shù)據(jù)存儲(chǔ)類型以及操作方式,對(duì)于一個(gè)實(shí)例對(duì)象中的成員方法而言,如果方法中包括本地變量是基本數(shù)據(jù)類型(boolean、type、short、char、int、long、float、double),將直接存儲(chǔ)在工作內(nèi)存的幀棧中,而對(duì)象實(shí)例將存儲(chǔ)在主內(nèi)存(共享數(shù)據(jù)區(qū)域,堆)中。但對(duì)于實(shí)例對(duì)象的成員變量,不管它是基本數(shù)據(jù)類型或者包裝類型(Integer、Double等)還是引用類型,都會(huì)被存儲(chǔ)到堆區(qū)。至于 static 變量以及類本身相關(guān)信息將會(huì)存儲(chǔ)在主內(nèi)存中。
需要注意的是,在主內(nèi)存中的實(shí)例對(duì)象可以被多線程共享,倘若兩個(gè)線程同時(shí)調(diào)用類同一個(gè)對(duì)象的同一個(gè)方法,那么兩個(gè)線程會(huì)將要操作的數(shù)據(jù)拷貝一份到直接的工作內(nèi)存中,執(zhí)行晚操作后才刷新到主內(nèi)存。模型如下圖所示:
Java 內(nèi)存模型與硬件內(nèi)存架構(gòu)的關(guān)系
通過(guò)對(duì)前面的硬件內(nèi)存架構(gòu)、Java內(nèi)存模型以及Java多線程的實(shí)現(xiàn)原理的了解,我們應(yīng)該已經(jīng)意識(shí)到,多線程的執(zhí)行最終都會(huì)映射到硬件處理器上進(jìn)行執(zhí)行,但Java內(nèi)存模型和硬件內(nèi)存架構(gòu)并不完全一致。對(duì)于硬件內(nèi)存來(lái)說(shuō)只有寄存器、緩存內(nèi)存、主內(nèi)存的概念,并沒(méi)有工作內(nèi)存(線程私有數(shù)據(jù)區(qū)域)和主內(nèi)存(堆內(nèi)存)之分,也就是說(shuō) Java 內(nèi)存模型對(duì)內(nèi)存的劃分對(duì)硬件內(nèi)存并沒(méi)有任何影響,因?yàn)?JMM 只是一種抽象的概念,是一組規(guī)則,并不實(shí)際存在,不管是工作內(nèi)存的數(shù)據(jù)還是主內(nèi)存的數(shù)據(jù),對(duì)于計(jì)算機(jī)硬件來(lái)說(shuō)都會(huì)存儲(chǔ)在計(jì)算機(jī)主內(nèi)存中,當(dāng)然也有可能存儲(chǔ)到 CPU 緩存或者寄存器中,因此總體上來(lái)說(shuō),Java 內(nèi)存模型和計(jì)算機(jī)硬件內(nèi)存架構(gòu)是一個(gè)相互交叉的關(guān)系,是一種抽象概念劃分與真實(shí)物理硬件的交叉。(注意對(duì)于Java內(nèi)存區(qū)域劃分也是同樣的道理)
JMM 存在的必要性
在明白了 Java 內(nèi)存區(qū)域劃分、硬件內(nèi)存架構(gòu)、Java多線程的實(shí)現(xiàn)原理與Java內(nèi)存模型的具體關(guān)系后,接著來(lái)談?wù)凧ava內(nèi)存模型存在的必要性。
由于JVM運(yùn)行程序的實(shí)體是線程,而每個(gè)線程創(chuàng)建時(shí) JVM 都會(huì)為其創(chuàng)建一個(gè)工作內(nèi)存(有些地方稱為棧空間),用于存儲(chǔ)線程私有的數(shù)據(jù),線程與主內(nèi)存中的變量操作必須通過(guò)工作內(nèi)存間接完成,主要過(guò)程是將變量從主內(nèi)存拷貝的每個(gè)線程各自的工作內(nèi)存空間,然后對(duì)變量進(jìn)行操作,操作完成后再將變量寫回主內(nèi)存,如果存在兩個(gè)線程同時(shí)對(duì)一個(gè)主內(nèi)存中的實(shí)例對(duì)象的變量進(jìn)行操作就有可能誘發(fā)線程安全問(wèn)題。
假設(shè)主內(nèi)存中存在一個(gè)共享變量 x ,現(xiàn)在有 A 和 B 兩個(gè)線程分別對(duì)該變量 x=1 進(jìn)行操作, A/B線程各自的工作內(nèi)存中存在共享變量副本 x 。假設(shè)現(xiàn)在 A 線程想要修改 x 的值為 2,而 B 線程卻想要讀取 x 的值,那么 B 線程讀取到的值是 A 線程更新后的值 2 還是更新錢的值 1 呢?
答案是:不確定。即 B 線程有可能讀取到 A 線程更新錢的值 1,也有可能讀取到 A 線程更新后的值 2,這是因?yàn)楣ぷ鲀?nèi)存是每個(gè)線程私有的數(shù)據(jù)區(qū)域,而線程 A 操作變量 x 時(shí),首先是將變量從主內(nèi)存拷貝到 A 線程的工作內(nèi)存中,然后對(duì)變量進(jìn)行操作,操作完成后再將變量 x寫回主內(nèi)存。而對(duì)于 B 線程的也是類似的,這樣就有可能造成主內(nèi)存與工作內(nèi)存間數(shù)據(jù)存在一致性問(wèn)題,假設(shè)直接的工作內(nèi)存中,這樣 B 線程讀取到的值就是 x=1 ,但是如果 A 線程已將 x=2 寫回主內(nèi)存后,B線程才開(kāi)始讀取的話,那么此時(shí) B 線程讀取到的就是 x=2 ,但到達(dá)是那種情況先發(fā)送呢?
如下圖所示案例:
以上關(guān)于主內(nèi)存與工作內(nèi)存直接的具體交互協(xié)議,即一個(gè)變量如何從主內(nèi)存拷貝到工作內(nèi)存,如何從工作內(nèi)存同步到主內(nèi)存之間的實(shí)現(xiàn)細(xì)節(jié),Java內(nèi)存模型定義來(lái)以下八種操作來(lái)完成。
數(shù)據(jù)同步八大原子操作
- lock(鎖定):作用于主內(nèi)存的變量,把一個(gè)變量標(biāo)記為一個(gè)線程獨(dú)占狀態(tài);
- unlock(解鎖):作用于主內(nèi)存的變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái),釋放后的變量才可以被其他線程鎖定;
- read(讀取):作用于主內(nèi)存的變量,把一個(gè)變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以后隨后的load工作使用;
- load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量;
- use(使用):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量值傳遞給執(zhí)行引擎;
- assign(賦值):作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量;
- store(存儲(chǔ)):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write的操作;
- wirte(寫入):作用于工作內(nèi)存的變量,它把store操作從工作內(nèi)存中的一個(gè)變量值傳送到主內(nèi)存的變量中。
- 如果要把一個(gè)變量從主內(nèi)存中復(fù)制到工作內(nèi)存中,就需要按順序地執(zhí)行 read 和 load 操作;
- 如果把變量從工作內(nèi)存中同步到主內(nèi)存中,就需要按順序地執(zhí)行 store 和 write 操作。
但Java 內(nèi)存模型只要求上述操作必須按順序執(zhí)行,而沒(méi)有保證必須是連續(xù)執(zhí)行。
同步規(guī)則分析
- 不允許一個(gè)線程無(wú)原因地(沒(méi)有發(fā)生任何 assign 操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中;
- 一個(gè)新的變量只能在主內(nèi)存中誕生,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load 或者 assign)的變量。即就是對(duì)一個(gè)變量實(shí)施 use 和 store 操作之前,必須先自行 assign 和 load 操作;
- 一個(gè)變量在同一時(shí)刻只允許一條線程對(duì)其進(jìn)行 lock 操作,但 lock 操作可不被同一線程重復(fù)執(zhí)行多次,多次執(zhí)行 lock 后,只有執(zhí)行相同次數(shù) unlock 操作,變量才會(huì)被解鎖。lock 和 unlock 必須成對(duì)出現(xiàn);
- 如果對(duì)一個(gè)變量執(zhí)行 lock 操作,將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用變量之前需要重新執(zhí)行 load 或 assign 操作初始化變量的值;
- 如果一個(gè)變量事先沒(méi)有被 lock 操作鎖定,則不允許對(duì)它執(zhí)行 unlock 操作;也不允許去 unlock 一個(gè)被其他線程鎖定的變量;
- 對(duì)一個(gè)變量執(zhí)行 unlock 操作之前,必須先把此變量同步到主內(nèi)存中(執(zhí)行store 和 write 操作)。
并發(fā)編程的可見(jiàn)性、原子性與有序性問(wèn)題
原子性
原子性指的是一個(gè)操作不可中斷,即使是在多線程環(huán)境下,一個(gè)操作一旦開(kāi)始就不會(huì)被其他線程影響。
在Java中,對(duì)于基本數(shù)據(jù)類型的變量的讀取和賦值操作是原子性操作需要注意的是:對(duì)于32位系統(tǒng)來(lái)說(shuō),long 類型數(shù)據(jù)和 double 類型數(shù)據(jù)(對(duì)于基本類型數(shù)據(jù):byte、short、int、float、boolean、char 讀寫是原子操作),它們的讀寫并非原子性的,也就是說(shuō)如果存在兩條線程同時(shí)對(duì) long 類型或者 double 類型的數(shù)據(jù)進(jìn)行讀寫是存在相互干擾的,因?yàn)閷?duì)于32位虛擬機(jī)來(lái)說(shuō),每次原子讀寫是32位,而 long 和 double 則是64位的存儲(chǔ)單元,這樣回導(dǎo)致一個(gè)線程在寫時(shí),操作完成前32位的原子操作后,輪到B線程讀取時(shí),恰好只讀取來(lái)后32位的數(shù)據(jù),這樣可能回讀取到一個(gè)即非原值又不是線程修改值的變量,它可能是“半個(gè)變量”的數(shù)值,即64位數(shù)據(jù)被兩個(gè)線程分成了兩次讀取。但也不必太擔(dān)心,因?yàn)樽x取到“半個(gè)變量”的情況比較少,至少在目前的商用虛擬機(jī)中,幾乎都把64位的數(shù)據(jù)的讀寫操作作為原子操作來(lái)執(zhí)行,因此對(duì)于這個(gè)問(wèn)題不必太在意,知道怎么回事即可。
- X=10; //原子性(簡(jiǎn)單的讀取、將數(shù)字賦值給變量)
- Y = x; //變量之間的相互賦值,不是原子操作
- X++; //對(duì)變量進(jìn)行計(jì)算操作
- X=x+1;
可見(jiàn)性
理解了指令重排現(xiàn)象后,可見(jiàn)性容易理解了??梢?jiàn)性指的是當(dāng)一個(gè)線程修改了某個(gè)共享變量的值,其他線程是否能夠馬上得知這個(gè)修改的值。對(duì)于串行程序來(lái)說(shuō),可見(jiàn)性是不存在的,因?yàn)槲覀冊(cè)谌魏我粋€(gè)操作中修改了某個(gè)變量的值,后續(xù)的操作中都能讀取到這個(gè)變量,并且是修改過(guò)的新值。
但在多線程環(huán)境中可就不一定了,前面我們分析過(guò),由于線程對(duì)共享變量的操作都是線程拷貝到各自的工作內(nèi)存進(jìn)行操作后才寫回到主內(nèi)存中的,這就可能存在一個(gè)線程A修改了共享變量 x 的值,還未寫回主內(nèi)存時(shí),另外一個(gè)線程B又對(duì)主內(nèi)存中同一個(gè)共享變量 x 進(jìn)行操作,但此時(shí)A線程工作內(nèi)存中共享變量 x 對(duì)線程B來(lái)說(shuō)并不可見(jiàn),這種工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象就會(huì)造成可見(jiàn)性問(wèn)題,另外指令重排以及編譯器優(yōu)化也可能回導(dǎo)致可見(jiàn)性問(wèn)題,通過(guò)前面的分析,我們知道無(wú)論是編譯器優(yōu)化還是處理器優(yōu)化的重排現(xiàn)象,在多線程環(huán)境下,確實(shí)回導(dǎo)致程序亂序執(zhí)行的問(wèn)題,從而也就導(dǎo)致可見(jiàn)性問(wèn)題。
有序性
有序性是指對(duì)于單線程的執(zhí)行代碼,我們總是認(rèn)為代碼的執(zhí)行是按順序依次執(zhí)行的,這樣的理解并沒(méi)有毛病,比較對(duì)于單線程而言確實(shí)如此,但對(duì)于多線程環(huán)境,則可能出現(xiàn)亂序現(xiàn)象,因?yàn)槌绦蚓幾g稱機(jī)器碼指令后可能回出現(xiàn)指令重排現(xiàn)象,重排后的指令與原指令的順序未必一致,要明白的是,在Java程序中,倘若在本線程內(nèi),所有操作都視為有序行為,如果是多線程環(huán)境下,一個(gè)線程中觀察另外一個(gè)線程,所有操作都是無(wú)序的,前半句指的是單線程內(nèi)保證串行語(yǔ)義執(zhí)行的一致性,后半句則指令重排現(xiàn)象和工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象。
JMM如何解決原子性、可見(jiàn)性和有序性問(wèn)題
原子性問(wèn)題
除了 JVM 自身提供的對(duì)基本數(shù)據(jù)類型讀寫操作的原子性外,可以通過(guò) synchronized 和 Lock 實(shí)現(xiàn)原子性。因?yàn)?synchronized 和 Lock 能夠保證任一時(shí)刻只有一個(gè)線程訪問(wèn)該代碼塊。
可見(jiàn)性問(wèn)題
volatile 關(guān)鍵字可以保證可見(jiàn)性。當(dāng)一個(gè)共享變量被 volatile 關(guān)鍵字修飾時(shí),它會(huì)保證修改的值立即被其他的線程看到,即修改的值立即更新到主存中,當(dāng)其他線程需要讀取時(shí),它會(huì)去內(nèi)存中讀取新值。synchronized 和 Lock 也可以保證可見(jiàn)性,因?yàn)樗鼈兛梢员WC任一時(shí)刻只有一個(gè)線程能訪問(wèn)共享資源,并在其釋放鎖之前將修改的變量刷新到內(nèi)存中。
有序性問(wèn)題
在Java里面,可以通過(guò) volatile 關(guān)鍵字來(lái)保證一定的“有序性”。另外可以通過(guò) synchronized 和 Lock 來(lái)保證有序性,很顯然,synchronized 和 Lock 保證每個(gè)時(shí)刻是只有一個(gè)線程執(zhí)行同步代碼,相當(dāng)于是讓線程順序執(zhí)行同步代碼,自然就保證來(lái)有序性。
Java內(nèi)存模型
每個(gè)線程都有自己的工作內(nèi)存,線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接對(duì)主內(nèi)存進(jìn)行操作。并且每個(gè)線程不能訪問(wèn)其他線程的工作內(nèi)存。Java 內(nèi)存模型具有一些先天的“有序性”,即不需要通過(guò)任何手段就能夠得到保證的有序性,這個(gè)通常也稱為 happens-before 原則。如果兩個(gè)操作的執(zhí)行次序無(wú)法從 happens-before 原則推導(dǎo)出來(lái),那么它們就不能保證它們的有序性,虛擬機(jī)可以隨意地對(duì)它們進(jìn)行重排序。
指令重排序
Java語(yǔ)言規(guī)范規(guī)定 JVM 線程內(nèi)部維持順序化語(yǔ)義。即只要程序的最終結(jié)果與它順序化情況的結(jié)果相等,那么指令的執(zhí)行順序可以與代碼順序不一致,此過(guò)程叫做指令的重排序。
指令重排序的意義是什么?JVM能根據(jù)處理特性(CPU多級(jí)緩存、多核處理器等)適當(dāng)?shù)膶?duì)機(jī)器指令進(jìn)行重排序,使機(jī)器指令更更符合CPU的執(zhí)行特性,最大限度的發(fā)揮機(jī)器性能。
下圖為從源碼到最終執(zhí)行的指令序列示意圖:
as-if-serial 語(yǔ)義
as-if-serial語(yǔ)義的意思是:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變。編譯器、runtime和處理器都必須遵守 as-if-serial 語(yǔ)義。
為了遵守 as-if-serial 語(yǔ)義,編譯器和處理器不會(huì)對(duì)存在數(shù)據(jù)依賴關(guān)系的操作做重排序,因?yàn)檫@種重排序會(huì)改變執(zhí)行結(jié)果。但是,如果操作之間不存在數(shù)據(jù)依賴關(guān)系,這些操作就可能被編譯器和處理器重排序。
happens-before 原則
只靠 synchronized 和 volatile 關(guān)鍵字來(lái)保證原子性、可見(jiàn)性以及有序性,那么編寫并發(fā)程序可能會(huì)顯得十分麻煩,幸運(yùn)的是,從JDK 5 開(kāi)始,Java 使用新的 JSR-133 內(nèi)存模型,提供了 happens-before 原則 來(lái)輔助保證程序執(zhí)行的原子性、可見(jiàn)性和有序性的問(wèn)題,它是判斷數(shù)據(jù)十分存在競(jìng)爭(zhēng)、線程十分安全的一句。happens-before 原則內(nèi)容如下:
- 程序順序原則,即在一個(gè)線程內(nèi)必須保證語(yǔ)義串行,也就是說(shuō)按照代碼順序執(zhí)行。
- 鎖規(guī)則,解鎖(unlock)操作必然發(fā)生在后續(xù)的同一個(gè)鎖的加鎖(lock)之前,也就是說(shuō),如果對(duì)于一個(gè)鎖解鎖后,再加鎖,那么加鎖的動(dòng)作必須在解鎖動(dòng)作之后(同一個(gè)鎖)。
- volatile規(guī)則, volatile變量的寫,先發(fā)生于讀,這保證了volatile變量的可見(jiàn)性,簡(jiǎn)單理解就是,volatile變量在每次被線程訪問(wèn)時(shí),都強(qiáng)迫從主內(nèi)存中讀該變量的值,而當(dāng)該變量發(fā)生變化時(shí),又會(huì)強(qiáng)迫將最新的值刷新到主內(nèi)存,任何時(shí)刻,不同的線程總是能夠看到該變量的最新值。
- 線程啟動(dòng)規(guī)則,線程的 start() 方法先于它的每一個(gè)動(dòng)作,即如果線程A在執(zhí)行線程B的 start 方法之前修改了共享變量的值,那么當(dāng)線程B執(zhí)行start方法時(shí),線程A對(duì)共享變量的修改對(duì)線程B可見(jiàn)。
- 傳遞性,A先于B,B先于C,那么A必然先于C。
- 線程終止原則,線程的所有操作先于線程的終結(jié),Thread.join() 方法的作用是等待當(dāng)前執(zhí)行的線程終止。假設(shè)在線程B終止之前,修改了共享變量,線程A從線程B的join方法成功返回,線程B對(duì)共享變量的修改將對(duì)線程A可見(jiàn)。
- 線程中斷規(guī)則,對(duì)線程 interrupt() 方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢查到中斷事件的發(fā)生,可以通過(guò) Thread.interrupted() 方法檢測(cè)線程十分中斷。
- 對(duì)象終結(jié)規(guī)則,對(duì)象的構(gòu)造函數(shù)執(zhí)行,結(jié)束先于 finalize() 方法。
finalize()是Object中的方法,當(dāng)垃圾回收器將要回收對(duì)象所占內(nèi)存之前被調(diào)用,即當(dāng)一個(gè)對(duì)象被虛擬機(jī)宣告死亡時(shí)會(huì)先調(diào)用它finalize()方法,讓此對(duì)象處理它生前的最后事情(這個(gè)對(duì)象可以趁這個(gè)時(shí)機(jī)掙脫死亡的命運(yùn))。
volatile 內(nèi)存語(yǔ)義
volatile 是Java虛擬機(jī)提供的輕量級(jí)的同步機(jī)制。volatile 關(guān)鍵字有如下兩個(gè)作用:
- 保證被 volatile 修飾的共享變量對(duì)所有線程總是可見(jiàn)的,也就是當(dāng)一個(gè)線程修改了被 volatile 修飾共享變量的值,新值總是可以被其他線程立即得知。
- 緊張指令重排序優(yōu)化。
volatile 的可見(jiàn)性
關(guān)于 volatile 的可見(jiàn)性作用,我們必須意思到被 volatile 修飾的變量對(duì)所有線程總是立即可見(jiàn)的,對(duì)于 volatile 變量的所有寫操作總是能立刻反應(yīng)到其他線程中。
案例:線程A改變 initFlag 屬性之后,線程B馬上感知到
- package com.niuh.jmm;
- import lombok.extern.slf4j.Slf4j;
- /**
- * @description: -server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*Jmm03_CodeVisibility.refresh
- * -Djava.compiler=NONE
- **/
- @Slf4j
- public class Jmm03_CodeVisibility {
- private static boolean initFlag = false;
- private volatile static int counter = 0;
- public static void refresh() {
- log.info("refresh data.......");
- initFlag = true;
- log.info("refresh data success.......");
- }
- public static void main(String[] args) {
- // 線程A
- Thread threadA = new Thread(() -> {
- while (!initFlag) {
- //System.out.println("runing");
- counter++;
- }
- log.info("線程:" + Thread.currentThread().getName()
- + "當(dāng)前線程嗅探到initFlag的狀態(tài)的改變");
- }, "threadA");
- threadA.start();
- // 中間休眠500hs
- try {
- Thread.sleep(500);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // 線程B
- Thread threadB = new Thread(() -> {
- refresh();
- }, "threadB");
- threadB.start();
- }
- }
結(jié)合前面介紹的數(shù)據(jù)同步八大原子操作,我們來(lái)分析下:
線程A啟動(dòng)后:
- 第一步:執(zhí)行read操作,作用于主內(nèi)存,將變量initFlag從主內(nèi)存拷貝一份,這時(shí)候還沒(méi)有放到工作內(nèi)存中,而是放在了總線里。如下圖
- 第二步:執(zhí)行l(wèi)oad操作,作用于工作內(nèi)存,將上一步拷貝的變量,放入工作內(nèi)存中;
- 第三步:執(zhí)行use(使用)操作,作用于工作內(nèi)存,把工作內(nèi)存中的變量傳遞給執(zhí)行引擎,對(duì)于線程A來(lái)說(shuō),執(zhí)行引擎會(huì)判斷initFlag = true嗎?不等于,循環(huán)一直進(jìn)行
執(zhí)行過(guò)程如下圖:
線程B啟動(dòng)后:
- 第一步:執(zhí)行read操作,作用于主內(nèi)存,從主內(nèi)存拷貝initFlag變量,這時(shí)候拷貝的變量還沒(méi)有放到工作內(nèi)存中,這一步是為了load做準(zhǔn)備;
- 第二步:執(zhí)行l(wèi)oad操作,作用于工作內(nèi)存,將拷貝的變量放入到工作內(nèi)存中;
- 第三步:執(zhí)行use操作,作用于工作內(nèi)存,將工作內(nèi)存的變量傳遞給執(zhí)行引擎,執(zhí)行引擎判斷while(!initFlag),那么執(zhí)行循環(huán)體;
- 第四步:執(zhí)行assign操作,作用于工作內(nèi)存,把從執(zhí)行引擎接收的值賦值給工作內(nèi)存的變量,即設(shè)置 inifFlag = true ;
- 第五步:執(zhí)行store操作,作用于工作內(nèi)存,將工作內(nèi)存中的變量 initFlag = true 傳遞給主內(nèi)存;
- 第六步:執(zhí)行write操作,作用于工作內(nèi)存,將變量寫入到主內(nèi)存中。
volatile 無(wú)法保證原子性
- //示例
- public class VolatileVisibility {
- public static volatile int i =0;
- public static void increase(){
- i++;
- }
- }
在并發(fā)場(chǎng)景下, i 變量的任何改變都會(huì)立馬反應(yīng)到其他線程中,但是如此存在多線程同時(shí)調(diào)用 increase() 方法的化,就會(huì)出現(xiàn)線程安全問(wèn)題,畢竟 i++ 操作并不具備原子性,該操作是先讀取值,然后寫回一個(gè)新值,相當(dāng)于原來(lái)的值加上1,分兩部完成。如果第二個(gè)線程在第一個(gè)線程讀取舊值和寫回新值期間讀取 i 的值,那么第二個(gè)線程就會(huì)于第一個(gè)線程一起看到同一個(gè)值,并執(zhí)行相同值的加1操作,這也就造成了線程安全失敗,因此對(duì)于 increase 方法必須使用 synchronized 修飾,以便保證線程安全,需要注意的是一旦使用 synchronized 修飾方法后,由于 sunchronized 本身也具備于 volatile 相同的特性,即可見(jiàn)性,因此在這樣的情況下就完全可以省去 volatile 修飾變量。
案例:起了10個(gè)線程,每個(gè)線程加到1000,10個(gè)線程,一共是10000
- package com.niuh.jmm;
- /**
- * volatile可以保證可見(jiàn)性, 不能保證原子性
- */
- public class Jmm04_CodeAtomic {
- private volatile static int counter = 0;
- static Object object = new Object();
- public static void main(String[] args) {
- for (int i = 0; i < 10; i++) {
- Thread thread = new Thread(() -> {
- for (int j = 0; j < 1000; j++) {
- synchronized (object) {
- counter++;//分三步- 讀,自加,寫回
- }
- }
- });
- thread.start();
- }
- try {
- Thread.sleep(3000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(counter);
- }
- }
而實(shí)際結(jié)果,不到10000, 原因是: 有并發(fā)操作.
這時(shí)候, 如果我在counter上加關(guān)鍵字volatile, 可以保證原子性么?
- private volatile static int counter = 0;
我們發(fā)現(xiàn), 依然不是10000, 這說(shuō)明volatile不能保證原子性.
每個(gè)線程, 只有一個(gè)操作, counter++, 為什么不能保證原子性呢?
其實(shí)counter++不是一步完成的. 他是分為多步完成的. 我們用下面的圖來(lái)解釋
線程A通過(guò)read, load將變量加載到工作內(nèi)存, 通過(guò)user將變量發(fā)送到執(zhí)行引擎, 執(zhí)行引擎執(zhí)行counter++,這時(shí)線程B啟動(dòng)了, 通過(guò)read, load將變量加載到工作內(nèi)存, 通過(guò)user將變量發(fā)送到執(zhí)行引擎, 然后執(zhí)行復(fù)制操作assign, stroe, write操作. 我們看到這是經(jīng)過(guò)了n個(gè)步驟. 雖然看起來(lái)就是簡(jiǎn)單的一句話.
當(dāng)線程B執(zhí)行store將數(shù)據(jù)回傳到主內(nèi)存的時(shí)候, 同時(shí)會(huì)通知線程A, 丟棄counter++, 而這時(shí)counter已經(jīng)自加了1, 將自加后的counter丟掉, 就導(dǎo)致總數(shù)據(jù)少1.
volatile 禁止重排優(yōu)化
volatile 關(guān)鍵字另一個(gè)作用就是禁止指令重排優(yōu)化,從而避免多線程環(huán)境下程序出現(xiàn)亂序執(zhí)行的現(xiàn)象,關(guān)于指令重排優(yōu)化前面已經(jīng)分析過(guò),這里主要簡(jiǎn)單說(shuō)明一下 volatile 是如何實(shí)現(xiàn)禁止指令重排優(yōu)化的。先了解一個(gè)概念,內(nèi)存屏障(Memory Barrier)
硬件層的內(nèi)存屏障
Intel 硬件提供了一系列的內(nèi)存屏障,主要又:
- ifence,是一種 Load Barrier 讀屏障;
- sfence,是一種 Store Barrier 寫屏障;
- mfence,是一種全能型的屏障,具備 ifence 和 sfence 的能力;
- Lock 前綴,Lock 不是一種內(nèi)存屏障,但是它能完成類似內(nèi)存屏障的功能。Lock 會(huì)對(duì) CPU總線和高速緩存加鎖,可以理解為 CPU 指令級(jí)的一種鎖。它后面可以跟 ADD、ADC、AND、BTC、BTR、BTS、CMPXCHG、CMPXCH8B、DEC、INC、NEG、NOT、OR、SBB、SUB、XOR、XADD、and XCHG 等指令。
不同硬件實(shí)現(xiàn)內(nèi)存屏障的方式不同,Java 內(nèi)存模型屏蔽了這些底層硬件平臺(tái)的差異,由 JVM 來(lái)為不同平臺(tái)生產(chǎn)相應(yīng)的機(jī)器碼。JVM中提供了四類內(nèi)存屏障指令:
內(nèi)存屏障,又稱內(nèi)存柵欄,是一個(gè)CPU指令,它的作用有兩個(gè):
- 一是保證特定操作的執(zhí)行順序;
- 二是保證某些變量的內(nèi)存可見(jiàn)性(利用該特性實(shí)現(xiàn) volatile 的內(nèi)存可見(jiàn)性)。
由于編譯器和處理器都能執(zhí)行指令重排優(yōu)化。如果在指令間插入一條 Memory Barrier 則會(huì)高速編譯器和CPU,不管什么指令都不能和這條 Memory Barrier 指令重排序,也就是說(shuō)通過(guò)插入內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行重排序優(yōu)化。
Memory Barrier 的另外一個(gè)作用是強(qiáng)制刷出各種 CPU 的緩存數(shù)據(jù),因此任何 CPU 上的線程都能讀取到這些數(shù)據(jù)的最新版本。
總之,volatile 變量正是通過(guò)內(nèi)存屏障實(shí)現(xiàn)其內(nèi)存中的語(yǔ)義,即可見(jiàn)性和禁止重排優(yōu)化。
下面看一個(gè)非常典型的禁止重排優(yōu)化的例子DCL,如下:
- public class DoubleCheckLock {
- private volatile static DoubleCheckLock instance;
- private DoubleCheckLock(){}
- public static DoubleCheckLock getInstance(){
- //第一次檢測(cè)
- if (instance==null){
- //同步
- synchronized (DoubleCheckLock.class){
- if (instance == null){
- //多線程環(huán)境下可能會(huì)出現(xiàn)問(wèn)題的地方
- instance = new DoubleCheckLock();
- }
- }
- }
- return instance;
- }
- }
上述代碼一個(gè)經(jīng)典的單例的雙重檢測(cè)的代碼,這段代碼在單線程環(huán)境下并沒(méi)什么問(wèn)題,但如果在多線程環(huán)境下就可能會(huì)出現(xiàn)線程安全的問(wèn)題。因?yàn)樵谟谀骋痪€程執(zhí)行到第一次檢測(cè),讀取到 instance 不為 null 時(shí),instance 的引用對(duì)象可能還沒(méi)有完成初始化。
- 關(guān)于 單例模式 可以查看 設(shè)計(jì)模式系列—單例設(shè)計(jì)模式
因?yàn)?instance = new DoubleCheckLock(); 可以分為以下3步完成(偽代碼)
- memory = allocate(); // 1.分配對(duì)象內(nèi)存空間
- instance(memory); // 2.初始化對(duì)象
- instance = memory; // 3.設(shè)置instance指向剛分配的內(nèi)存地址,此時(shí)instance != null
由于步驟1 和步驟2 間可能會(huì)重排序,如下:
- memory=allocate();//1.分配對(duì)象內(nèi)存空間
- instance=memory;//3.設(shè)置instance指向剛分配的內(nèi)存地址,此時(shí)instance!=null,但是對(duì)象還沒(méi)有初始化完成!
- instance(memory);//2.初始化對(duì)象
由于步驟2和步驟3不存在數(shù)據(jù)依賴關(guān)系,而且無(wú)論重排前還是重排后程序的指向結(jié)果在單線程中并沒(méi)有改變,因此這種重排優(yōu)化是允許的。但是指令重排只會(huì)保證串行語(yǔ)義執(zhí)行的一致性(單線程),但并不會(huì)關(guān)心多線程間的語(yǔ)義一致性。所以當(dāng)一條線程訪問(wèn) instance 不為 null 時(shí),由于 instance 實(shí)例未必已經(jīng)初始化完成,也就造成來(lái)線程安全問(wèn)題。那么該如何解決呢,很簡(jiǎn)單,我們使用 volatile 禁止 instance 變量被執(zhí)行指令重排優(yōu)化即可。
- //禁止指令重排優(yōu)化
- private volatile static DoubleCheckLock instance;
volatile 內(nèi)存語(yǔ)義的實(shí)現(xiàn)
前面提到過(guò)重排序分為編譯器重排序和處理器重排序。為來(lái)實(shí)現(xiàn) volatile 內(nèi)存語(yǔ)義,JMM 會(huì)分別限制這兩種類型的重排序類型。
下面是JMM針對(duì)編譯器制定的 volatile 重排序規(guī)則表。
舉例來(lái)說(shuō),第二行最后一個(gè)單元格的意思是:在程序中,當(dāng)?shù)谝粋€(gè)操作為普通變量的讀或者寫時(shí),如果第二個(gè)操作為 volatile 寫,則編譯器不能重排序這兩個(gè)操作。
從上圖可以看出:
- 當(dāng)?shù)诙€(gè)操作是 volatile 寫時(shí),不管第一個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保了 volatile 寫之前的操作不會(huì)被編譯器重排序到 volatile 寫之后。
- 當(dāng)?shù)谝粋€(gè)操作是 volatile 讀時(shí),不管第二個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保了 volatile 讀之后的操作不會(huì)被編譯器重排序到 volatie 讀之前。
- 當(dāng)?shù)谝粋€(gè)操作是 volatile 寫,第二個(gè)操作是 volatile 讀或?qū)憰r(shí),不能重排序。
為了實(shí)現(xiàn) volatile 的內(nèi)存語(yǔ)義,編譯在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來(lái)禁止特定類型的處理器重排序。對(duì)于編譯器來(lái)說(shuō),發(fā)現(xiàn)一個(gè)最優(yōu)布置來(lái)最小化插入屏障的總數(shù)幾乎不可能。為此,JMM 采取保守策略。下面是基于保守策略的JMM內(nèi)存屏障插入策略。
- 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障;
- 在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障;
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障;
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障;
上述內(nèi)存屏障插入策略非常保守,但它可以保證在任一處理器平臺(tái),任意的程序中都能得到正確的 volatile 內(nèi)存語(yǔ)義。
下面是保守策略下,volatile 寫插入內(nèi)存屏障后生成的指令序列示意圖
上圖中 StoreStore 屏障可以保證在volatile 寫之前,其前面的所有普通寫操作已經(jīng)對(duì)任意處理器可見(jiàn)來(lái)。這是因?yàn)镾toreStore屏障將保障上面所有的普通寫在 volatile 寫之前刷新到主內(nèi)存。
這里比較有意思的是,volatile 寫后面的 StoreLoad 屏障。此屏障的作用是避免 volatile 寫與后面可能有的 volatile 讀/寫操作重排序。因?yàn)榫幾g器常常無(wú)法準(zhǔn)確判斷在一個(gè) volatile 寫的后面十分需要插入一個(gè) StoreLoad 屏障(比如,一個(gè)volatile寫之后方法立即return)。為來(lái)保證能正確實(shí)現(xiàn) volatile 的內(nèi)存語(yǔ)義,JMM 在采取了保守策略:在每個(gè) volatile 寫的后面,或者每個(gè) volatile 讀的前面插入一個(gè) StoreLoad 屏障。從整體執(zhí)行效率的角度考慮,JMM最終選擇了在每個(gè) volatile 寫的后面插入一個(gè) StoreLoad 屏障,因?yàn)関olatile寫-讀內(nèi)存語(yǔ)義的常見(jiàn)使用模式是:一個(gè)寫線程寫 volatile 變量,多個(gè)線程讀同一個(gè) volatile 變量。當(dāng)讀線程的數(shù)量大大超過(guò)寫線程時(shí),選擇在 volatile 寫之后插入 StoreLoad 屏障將帶來(lái)可觀的執(zhí)行效率的提升。從這里可以看到JMM在實(shí)現(xiàn)上的一個(gè)特點(diǎn):首先確保正確性,然后再去追求執(zhí)行效率。
下圖是在保守策略下,volatile 讀插入內(nèi)存屏障后生成的指令序列示意圖
上圖中 LoadLoad 屏障用來(lái)禁止處理器把上面的 volatile讀 與下面的普通讀重排序。LoadStore 屏障用來(lái)禁止處理器把上面的volatile讀與下面的普通寫重排序。
上述 volatile寫 和 volatile讀的內(nèi)存屏障插入策略非常保守。在實(shí)際執(zhí)行時(shí),只要不改變 volatile寫-讀的內(nèi)存語(yǔ)義,編譯器可以根據(jù)具體情況省略不必要的屏障。
下面通過(guò)具體的示例代碼進(jìn)行說(shuō)明。
- class VolatileBarrierExample {
- int a;
- volatile int v1 = 1;
- volatile int v2 = 2;
- void readAndWrite() {
- int i = v1; // 第一個(gè)volatile讀
- int j = v2; // 第二個(gè)volatile讀
- a = i + j; // 普通寫
- v1 = i + 1; // 第一個(gè)volatile寫
- v2 = j * 2; // 第二個(gè) volatile寫
- }
- }
針對(duì) readAndWrite()方法,編譯器在生成字節(jié)碼時(shí)可以做如下的優(yōu)化。
注意,最后的 StoreLoad 屏障不能省略。因?yàn)榈诙€(gè) volatile 寫之后,方法立即 return。此時(shí)編譯器可能無(wú)法準(zhǔn)確判斷斷定后面十分會(huì)有 volatile 讀或?qū)?,為了安全起?jiàn),編譯器通常會(huì)在這里插入一個(gè) StoreLoad 屏障。
上面的優(yōu)化針對(duì)任意處理器平臺(tái),由于不同的處理器有不同“松緊度”的處理器內(nèi)存模型,內(nèi)存屏障的插入還可以根據(jù)具體的處理器內(nèi)存模型繼續(xù)優(yōu)化。以X86處理完為例,上圖中除最后的 StoreLoad 屏障外,其他的屏障都會(huì)被省略。
前面保守策略下的 volatile 讀和寫,在 X86 處理器平臺(tái)可以優(yōu)化如下圖所示。X86處理器僅會(huì)對(duì)讀-寫操作做重排序。X86 不會(huì)對(duì)讀-讀、讀-寫 和 寫-寫 做重排序,因此在 X86 處理器中會(huì)省略掉這3種操作類型對(duì)應(yīng)的內(nèi)存屏障。在 X86 中,JMM僅需在 volatile 寫后面插入一個(gè) StoreLoad 屏障即可正確實(shí)現(xiàn) volatile寫-讀的內(nèi)存語(yǔ)義,這意味著在 X86 處理器中,volatile寫的開(kāi)銷比volatile讀的開(kāi)銷會(huì)大很多(因?yàn)閳?zhí)行StoreLoad的屏障開(kāi)銷會(huì)比較大)
參考資料
- 《并發(fā)編程的藝術(shù)》
PS:以上代碼提交在 Github :
https://github.com/Niuh-Study/niuh-juc-final.git
標(biāo)題名稱:深入理解Java內(nèi)存模型(JMM)及Volatile關(guān)鍵字
本文路徑:http://fisionsoft.com.cn/article/dhjihie.html


咨詢
建站咨詢
