首頁技術(shù)文章正文

Jvm內(nèi)存模型深入介紹[java培訓(xùn)]

更新時間:2020-03-15 來源:黑馬程序員 瀏覽量:

Web服務(wù)端是Java語言最擅長的領(lǐng)域之一,也會Java最廣泛應(yīng)用的地方。而高并發(fā)高吞吐量也越來越成為服務(wù)端普遍需求,所有能夠開發(fā)出高效并發(fā)的應(yīng)用程序,也是成為一個高級程序員的必備技能。下面我們將從JVM內(nèi)存模型的角度來分析虛擬機如何實現(xiàn)多線程、多線程之間由于共享和競爭數(shù)據(jù)而導(dǎo)致的并發(fā)問題及解決思路。

計算機硬件內(nèi)存架構(gòu)
想要了解JVM內(nèi)存模型,我們需要先了解下計算機的硬件內(nèi)存架構(gòu)
jvm內(nèi)存架構(gòu)模型01
正如上圖所示,經(jīng)過簡化CPU與內(nèi)存操作的簡易圖,實際上沒有這么簡單,這里為了理解方便,我們省去了南北橋并將三級緩存統(tǒng)一為CPU緩存(有些CPU只有二級緩存,有些CPU有三級緩存)。就目前計算機而言,一般擁有多個CPU并且每個CPU可能存在多個核心,多核是指在一枚處理器(CPU)中集成兩個或多個完整的計算引擎(內(nèi)核),這樣就可以支持多任務(wù)并行執(zhí)行,從多線程的調(diào)度來說,每個線程都會映射到各個CPU核心中并行運行。在CPU內(nèi)部有一組CPU寄存器,寄存器是CPU直接訪問和處理的數(shù)據(jù),是一個臨時放數(shù)據(jù)的空間。一般CPU都會從內(nèi)存取數(shù)據(jù)到寄存器,然后進(jìn)行處理,但由于內(nèi)存的處理速度遠(yuǎn)遠(yuǎn)低于CPU,導(dǎo)致CPU在處理指令時往往花費很多時間在等待內(nèi)存做準(zhǔn)備工作,于是在寄存器和主內(nèi)存間添加了CPU緩存,CPU緩存比較小,但訪問速度比主內(nèi)存快得多,用它來作為內(nèi)存與處理器之間的緩沖:將運算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運算能快速進(jìn)行,當(dāng)運算結(jié)束后再從緩存同步到內(nèi)存之中,這樣處理器就不用等待緩慢的內(nèi)存讀寫了。

基于高速緩存的存儲交互很好的解決了處理器與內(nèi)存的速度矛盾,但也為計算機系統(tǒng) 帶來了更高的復(fù)雜度,因為它引入了一個新的問題:緩存一致性。在多處理器系統(tǒng)中,每個處理器都有自己的高速緩存,而它們又共享同一主內(nèi)存(RAM)。當(dāng)多個處理器的運算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時,將可能導(dǎo)致各自的緩存數(shù)據(jù)不一致,為了解決一致性問題,需要各個處理器訪問緩存時都遵循一些協(xié)議,在讀寫時根據(jù)協(xié)議來進(jìn)行操作,這些協(xié)議有MSI、MESI、MOSI等。被稱為硬件的“內(nèi)存模型”,可以理解為在特定的操作協(xié)議下,對特定的內(nèi)存或高速緩存進(jìn)行讀寫訪問的過程抽象。不同架構(gòu)的物理機器可以擁有不一樣的內(nèi)存模型,而我們的JAVA虛擬機也有自己的內(nèi)存模型。

Java線程與硬件處理器
了解完硬件的內(nèi)存架構(gòu)后,接著了解JVM中線程的實現(xiàn)原理,理解線程的實現(xiàn)原理,有助于我們了解Java內(nèi)存模型與硬件內(nèi)存架構(gòu)的關(guān)系,在Window系統(tǒng)和Linux系統(tǒng)上,Java線程的實現(xiàn)是基于一對一的線程模型,所謂的一對一模型,實際上就是通過語言級別層面程序去間接調(diào)用系統(tǒng)內(nèi)核的線程模型,即我們在使用Java線程時,Java虛擬機內(nèi)部是轉(zhuǎn)而調(diào)用當(dāng)前操作系統(tǒng)的內(nèi)核線程來完成當(dāng)前任務(wù)。這里需要了解一個術(shù)語,內(nèi)核線程(Kernel-Level Thread,KLT),它是由操作系統(tǒng)內(nèi)核(Kernel)支持的線程,這種線程是由操作系統(tǒng)內(nèi)核來完成線程切換,內(nèi)核通過操作調(diào)度器進(jìn)而對線程執(zhí)行調(diào)度,并將線程的任務(wù)映射到各個處理器上。每個內(nèi)核線程可以視為內(nèi)核的一個分身,這也就是操作系統(tǒng)可以同時處理多任務(wù)的原因。由于我們編寫的多線程程序?qū)儆谡Z言層面的,程序一般不會直接去調(diào)用內(nèi)核線程,取而代之的是一種輕量級的進(jìn)程(Light Weight Process),也是通常意義上的線程,由于每個輕量級進(jìn)程都會映射到一個內(nèi)核線程,因此我們可以通過輕量級進(jìn)程調(diào)用內(nèi)核線程,進(jìn)而由操作系統(tǒng)內(nèi)核將任務(wù)映射到各個處理器,這種輕量級進(jìn)程與內(nèi)核線程間1對1的關(guān)系就稱為一對一的線程模型。

Jvm內(nèi)存模型03


Java內(nèi)存模型
內(nèi)存模型概述
Java內(nèi)存模型(即Java Memory Model,簡稱JMM)本身是一種抽象的概念,并不真實存在,它描述的是一組規(guī)則或規(guī)范,通過這組規(guī)范定義了程序中各個變量(包括實例字段,靜態(tài)字段和構(gòu)成數(shù)組對象的元素)的訪問方式。

Java內(nèi)存模型的主要目標(biāo)是定義程序中的各個變量的訪問規(guī)則,即如何在虛擬機中將變量存儲到內(nèi)存和從內(nèi)存中取出。此處的變量不包括局部變量和方法參數(shù),因為它們是線程私有的,不會被共享,自然不存在競爭問題。由于JVM運行程序的實體是線程,而每個線程創(chuàng)建時JVM都會為其創(chuàng)建一個工作內(nèi)存(有些地方稱為??臻g),用于存儲線程私有的數(shù)據(jù),而Java內(nèi)存模型中規(guī)定所有變量都存儲在主內(nèi)存,主內(nèi)存是共享內(nèi)存區(qū)域,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內(nèi)存中進(jìn)行,首先要將變量從主內(nèi)存拷貝的自己的工作內(nèi)存空間,然后對變量進(jìn)行操作,操作完成后再將變量寫回主內(nèi)存,不能直接操作主內(nèi)存中的變量,工作內(nèi)存中存儲著主內(nèi)存中的變量副本拷貝,前面說過,工作內(nèi)存是每個線程的私有數(shù)據(jù)區(qū)域,因此不同的線程間無法訪問對方的工作內(nèi)存,線程間的通信(傳值)必須通過主內(nèi)存來完成,線程、主內(nèi)存、工作內(nèi)存三者的關(guān)系如下圖

jvm內(nèi)存模型04

弄清楚主內(nèi)存和工作內(nèi)存后,接了解一下主內(nèi)存與工作內(nèi)存的數(shù)據(jù)存儲類型以及操作方式,根據(jù)虛擬機規(guī)范,對于一個實例對象中的成員方法而言,如果方法中包含本地變量是基本數(shù)據(jù)類型(boolean,byte,short,char,int,long,float,double),將直接存儲在工作內(nèi)存的幀棧結(jié)構(gòu)中,但倘若本地變量是引用類型,那么該變量的引用會存儲在功能內(nèi)存的幀棧中,而對象實例將存儲在主內(nèi)存(共享數(shù)據(jù)區(qū)域,堆)中。但對于實例對象的成員變量,不管它是基本數(shù)據(jù)類型或者包裝類型(Integer、Double等)還是引用類型,都會被存儲到堆區(qū)。至于static變量以及類本身相關(guān)信息將會存儲在主內(nèi)存中。需要注意的是,在主內(nèi)存中的實例對象可以被多線程共享,倘若兩個線程同時調(diào)用了同一個對象的同一個方法,那么兩條線程會將要操作的數(shù)據(jù)拷貝一份到自己的工作內(nèi)存中,執(zhí)行完成操作后才刷新到主內(nèi)存。

JAVA內(nèi)存模型與JAVA內(nèi)存區(qū)域關(guān)系

這里需要注意下JAVA內(nèi)存模型中的主內(nèi)存、工作內(nèi)存與JAVA內(nèi)存區(qū)域中的JAVA堆、棧、方法區(qū)不是同一層次的內(nèi)存劃分,不要混淆。

JAVA內(nèi)存模型
·主內(nèi)存
主要存儲的是Java實例對象,所有線程創(chuàng)建的實例對象都存放在主內(nèi)存中,不管該實例對象是成員變量還是方法中的本地變量(也稱局部變量),當(dāng)然也包括了共享的類信息、常量、靜態(tài)變量。由于是共享數(shù)據(jù)區(qū)域,多條線程對同一個變量進(jìn)行訪問可能會發(fā)現(xiàn)線程安全問題。
·工作內(nèi)存
主要存儲當(dāng)前方法的所有本地變量信息(工作內(nèi)存中存儲著主內(nèi)存中的變量副本拷貝),每個線程只能訪問自己的工作內(nèi)存,即線程中的本地變量對其它線程是不可見的,就算是兩個線程執(zhí)行的是同一段代碼,它們也會各自在自己的工作內(nèi)存中創(chuàng)建屬于當(dāng)前線程的本地變量,當(dāng)然也包括了字節(jié)碼行號指示器、相關(guān)Native方法的信息。注意由于
工作內(nèi)存是每個線程的私有數(shù)據(jù),線程間無法相互訪問工作內(nèi)存,因此存儲在工作內(nèi)存的數(shù)據(jù)不存在線程安全問題。


Java內(nèi)存區(qū)域
jvm內(nèi)存模型06
·方法區(qū)(Method Area)
方法區(qū)屬于線程共享的內(nèi)存區(qū)域,又稱Non-Heap(非堆),主要用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù),根據(jù)Java 虛擬機規(guī)范的規(guī)定,當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時,將拋出OutOfMemoryError 異常。值得注意的是在方法區(qū)中存在一個叫運行時常量池(Runtime Constant Pool)的區(qū)域,它主要用于存放編譯器生成的各種字面量和符號引用,這些內(nèi)容將在類加載后存放到運行時常量池中,以便后續(xù)使用。

·JVM堆(Java Heap)
Java 堆也是屬于線程共享的內(nèi)存區(qū)域,它在虛擬機啟動時創(chuàng)建,是Java 虛擬機所管理的內(nèi)存中最大的一塊,主要用于存放對象實例,幾乎所有的對象實例都在這里分配內(nèi)存,注意Java 堆是垃圾收集器管理的主要區(qū)域,因此很多時候也被稱做GC 堆,如果在堆中沒有內(nèi)存完成實例分配,并且堆也無法再擴展時,將會拋出OutOfMemoryError異常。
·程序計數(shù)器(Program Counter Register)
屬于線程私有的數(shù)據(jù)區(qū)域,是一小塊內(nèi)存空間,主要代表當(dāng)前線程所執(zhí)行的字節(jié)碼行號指示器。字節(jié)碼解釋器工作時,通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計數(shù)器來完成。
·虛擬機棧(Java Virtual Machine Stacks)
屬于線程私有的數(shù)據(jù)區(qū)域,與線程同時創(chuàng)建,總數(shù)與線程關(guān)聯(lián),代表Java方法執(zhí)行的內(nèi)存模型。每個方法執(zhí)行時都會創(chuàng)建一個棧楨來存儲方法的的變量表、操作數(shù)棧、動態(tài)鏈接方法、返回值、返回地址等信息。每個方法從調(diào)用直結(jié)束就對于一個棧楨在虛擬機棧中的入棧和出棧過程。
·本地方法棧(Native Method Stacks)
本地方法棧屬于線程私有的數(shù)據(jù)區(qū)域,這部分主要與虛擬機用到的 Native 方法相關(guān),一般情況下,我們無需關(guān)心此區(qū)域。

Java內(nèi)存模型與硬件內(nèi)存架構(gòu)的關(guān)系
通過對前面的硬件內(nèi)存架構(gòu)、Java內(nèi)存模型以及Java多線程的實現(xiàn)原理的了解,我們應(yīng)該已經(jīng)意識到,多線程的執(zhí)行最終都會映射到硬件處理器上進(jìn)行執(zhí)行,但Java內(nèi)存模型和硬件內(nèi)存架構(gòu)并不完全一致。對于硬件內(nèi)存來說只有寄存器、緩存內(nèi)存、主內(nèi)存的概念,并沒有工作內(nèi)存(線程私有數(shù)據(jù)區(qū)域)和主內(nèi)存(堆內(nèi)存)之分,也就是說Java內(nèi)存模型對內(nèi)存的劃分對硬件內(nèi)存并沒有任何影響,因為JMM只是一種抽象的概念,是一組規(guī)則,并不實際存在,不管是工作內(nèi)存的數(shù)據(jù)還是主內(nèi)存的數(shù)據(jù),對于計算機硬件來說都會存儲在計算機主內(nèi)存中,當(dāng)然也有可能存儲到CPU緩存或者寄存器中,因此總體上來說,Java內(nèi)存模型和計算機硬件內(nèi)存架構(gòu)是一個相互交叉的關(guān)系,是一種抽象概念劃分與真實物理硬件的交叉。(注意對于Java內(nèi)存區(qū)域劃分也是同樣的道理)
jvm內(nèi)存模型07
JMM存在的必要性
在明白了Java內(nèi)存區(qū)域劃分、硬件內(nèi)存架構(gòu)、Java多線程的實現(xiàn)原理與Java內(nèi)存模型的具體關(guān)系后,接著來談?wù)凧ava內(nèi)存模型存在的必要 性。由于JVM運行程序的實體是線程,而每個線程創(chuàng)建時JVM都會為其創(chuàng)建一個工作內(nèi)存(有些地方稱為??臻g),用于存儲線程私有的數(shù)據(jù),線程與主內(nèi)存中 的變量操作必須通過工作內(nèi)存間接完成,主要過程是將變量從主內(nèi)存拷貝的每個線程各自的工作內(nèi)存空間,然后對變量進(jìn)行操作,操作完成后再將變量寫回主內(nèi)存, 如果存在兩個線程同時對一個主內(nèi)存中的實例對象的變量進(jìn)行操作就有可能誘發(fā)線程安全問題。

如下圖,主內(nèi)存中存在一個共享變量x,現(xiàn)在有A和B兩條線程分別對該變量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,這是因為 工作內(nèi)存是每個線程私有的數(shù)據(jù)區(qū)域,而線程A變量x時,首先是將變量從主內(nèi)存拷貝到A線程的工作內(nèi)存中,然后對變量進(jìn)行操作,操作完成后再將變量x寫回主 內(nèi),而對于B線程的也是類似的,這樣就有可能造成主內(nèi)存與工作內(nèi)存間數(shù)據(jù)存在一致性問題,假如A線程修改完后正在將數(shù)據(jù)寫回主內(nèi)存,而B線程此時正在讀取 主內(nèi)存,即將x=1拷貝到自己的工作內(nèi)存中,這樣B線程讀取到的值就是x=1,但如果A線程已將x=2寫回主內(nèi)存后,B線程才開始讀取的話,那么此時B線 程讀取到的就是x=2,但到底是哪種情況先發(fā)生呢?這是不確定的,這也就是所謂的線程安全問題。
jvm內(nèi)存模型08
為了解決類似上述的問題,JVM定義了一組規(guī)則,通過這組規(guī)則來決定一個線程對共享變量的寫入何時對另一個線程可見,這組規(guī)則也稱為Java內(nèi)存模型(即JMM),JMM是圍繞著程序執(zhí)行的原子性、有序性、可見性展開的,下面我們看看這三個特性。

內(nèi)存間交互操作
關(guān)于主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,即一個變量如何從主內(nèi)存拷貝到工作內(nèi) 存、如何從工作內(nèi)存同步回主內(nèi)存之類的實現(xiàn)細(xì)節(jié),Java內(nèi)存模型中定義了以下8種操作來完成,虛擬機實現(xiàn)時必須保證下面提及的每一種操作都是原子的、不可再分的。lock(鎖定):作用于主內(nèi)存的變量,它把一個變量標(biāo)識為一條線程獨占的狀態(tài)。
unlock(解鎖):作用于主內(nèi)存的變量,它把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
read(讀?。鹤饔糜谥鲀?nèi)存的變量,它把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動作使用。
load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
use(使用):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機遇到一個需要使用到變量的值的字節(jié)碼指令時將會執(zhí)行這個操作。
assign(賦值):作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。
store(存儲):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳送到主內(nèi)存中,以便隨后的write操作使用。
write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。
如果要把一個變量從主內(nèi)存復(fù)制到工作內(nèi)存,那就要順序地執(zhí)行read和load操作,如果要把變量從工作內(nèi)存同步回主內(nèi)存,就要順序地執(zhí)行store和write操作。注意,Java內(nèi)存模型只要求上述兩個操作必須按順序執(zhí)行,而沒有保證是連續(xù)執(zhí)行。也就是說,read與load之間、store與write之間是可插入其他指令的,如對主內(nèi)存中的變量a、b進(jìn)行訪問時,一種可能出現(xiàn)順序是read a、read b、load b、load a。除此之外,Java內(nèi)存模型還規(guī)定了在執(zhí)行上述8種基本操作時必須滿足如下規(guī)則:
·不允許read和load、store和write操作之一單獨出現(xiàn),即不允許一個變量從主內(nèi)存讀取了但工作內(nèi)存不接受,或者從工作內(nèi)存發(fā)起回寫了但主內(nèi)存不接受的情況出現(xiàn)。
·不允許一個線程丟棄它的最近的assign操作,即變量在工作內(nèi)存中改變了之后必須把該變化同步回主內(nèi)存。
·不允許一個線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從線程的工作內(nèi)存同步回主內(nèi)存中。
·一個新的變量只能在主內(nèi)存中“誕生”,不允許在工作內(nèi)存中直接使用一個未被初始化(load或assign)的變量,換句話說,就是對一個變量實施use、store操作之前,必須先執(zhí)行過了assign和load操作。
·一個變量在同一個時刻只允許一條線程對其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會被解鎖。
·如果對一個變量執(zhí)行l(wèi)ock操作,那將會清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個變量前,需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值
·如果一個變量事先沒有被lock操作鎖定,那就不允許對它執(zhí)行unlock操作,也不允許去unlock一個被其他線程鎖定住的變量。
·對一個變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)。
這8種內(nèi)存訪問操作以及上述規(guī)則限定,再加上稍后介紹的對volatile的一些特殊規(guī)定,就已經(jīng)完全確定了Java程序中哪些內(nèi)存訪問操作在并發(fā)下是安全的。由于這種定義相當(dāng)嚴(yán)謹(jǐn)?shù)质譄┈?,實踐起來很麻煩,所以在12.3.6節(jié)中筆者將介紹這種定義的一個等效判斷原則——先行發(fā)生原則,用來確定一個訪問在并發(fā)環(huán)境下是否安全。

原子性、可見性與有序性
原子性
原子性指的是一個操作是不可中斷的,即使是在多線程環(huán)境下,一個操作一旦開始就不會被其他線程影響。由Java內(nèi)存模型來直接保證的原子性變量操作包括read、load、assign、use、store和write,我們大致可以認(rèn)為基本數(shù)據(jù)類型的訪問讀寫是具備原子性的,但是對于64位的數(shù)據(jù)類型(long和double),在模型中特別定義了一條相對寬松的規(guī)定:允許虛擬機將沒有被volatile修飾的64位數(shù)據(jù)的讀寫操作劃分為兩次32位的操作來進(jìn)行,這樣會導(dǎo)致一個線程在寫時,操作完前32位的原子操作后,輪到B線程讀取時,恰好只讀取到了后32位的數(shù)據(jù),這樣可能會讀取到一個既非原值又不是線程修改值的變量,它可能是“半個變量”的數(shù)值,即64位數(shù)據(jù)被兩個線程分成了兩次讀取。但也不必太擔(dān)心,因為讀取到“半個變量”的情況比較少 見,至少在目前的商用的虛擬機中,幾乎都把64位的數(shù)據(jù)的讀寫操作作為原子操作來執(zhí)行,因此對于這個問題不必太在意,知道這么回事即可。
如果應(yīng)用場景需要一個更大范圍的原子性保證(經(jīng)常會遇到),Java內(nèi)存模型還提供了lock和unlock操作來滿足這種需求,盡管虛擬機未把lock和unlock操作直接開放給用戶使用,但是卻提供了更高層次的字節(jié)碼指令monitorenter和monitorexit來隱式地使用這兩個操作,這兩個字節(jié)碼指令反映到Java代碼中就是同步塊——synchronized關(guān)鍵字,因此在synchronized塊之間的操作也具備原子性。

可見性
可見性是指當(dāng)一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。對于串行程序來說,可見性問題是不存在的,因為我們在任何一個操作中修改了某個變量的值,后續(xù)的操作中都能讀取這個變量值,并且是修改過的新值。但在多線程環(huán)境中可就不一定了, 前面我們分析過,由于線程對共享變量的操作都是線程拷貝到各自的工作內(nèi)存進(jìn)行操作后才寫回到主內(nèi)存中的,這就可能存在一個線程A修改了共享變量x的值,還未寫回主內(nèi)存時,另外一個線程B又對主內(nèi)存中同一個共享變量x進(jìn)行操作,但此時A線程工作內(nèi)存中共享變量x對線程B來說并不可見,這種工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象就會造成可見性問題。

有序性
有序性是指對于單線程的執(zhí)行代碼,我們總是認(rèn)為代碼的執(zhí)行是按順序依次執(zhí)行的,這樣的理解并沒有毛病,畢竟對于單線程而言確實如此,但對于多線程環(huán)境,則可能出現(xiàn)亂序現(xiàn)象,因為程序編譯成機器碼指令后可能會出現(xiàn)指令重排現(xiàn)象,重排后的指令與原指令的順序未必一致,要明白的是,在Java程序中,倘若在本線程內(nèi),所有操作都視為有序行為,如果是多線程環(huán)境下,一個線程中觀察另外一個線程,所有操作都是無序的,前半句指的是單線程內(nèi)保證串行語義執(zhí)行的一 致性,后半句則指指令重排現(xiàn)象和工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象。

JMM提供的解決方案
在理解了原子性,可見性以及有序性問題后,看看JMM是如何保證的,在Java內(nèi)存模型中都提供一套解決方案供Java工程師在開發(fā)過程使用,如原子性問題,除了JVM自身提供的對基本數(shù)據(jù)類型讀寫操作的原子性外,對于方法級別或者代碼塊級別的原子性操作,可以使用synchronized關(guān)鍵字或 者重入鎖(ReentrantLock)保證程序執(zhí)行的原子性。 而工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象導(dǎo)致的可見性問題,可以使用synchronized關(guān)鍵字或者volatile關(guān)鍵字解決,它們都可以使一個線程修改后 的變量立即對其他線程可見。對于指令重排導(dǎo)致的可見性問題和有序性問題,則可以利用volatile關(guān)鍵字解決,因為volatile的另外一個作用就是 禁止重排序優(yōu)化,關(guān)于volatile稍后會進(jìn)一步分析。除了靠sychronized和volatile關(guān)鍵字來保證原子性、可見性以及有序性外,JMM內(nèi)部還定義一套happens-before原則來保證多線程環(huán)境下兩個操作間的原子性、可見性以及有序性。推薦了解黑馬程序員java培訓(xùn)課程

volatile內(nèi)存語義
volatile是Java虛擬機提供的輕量級的同步機制。volatile關(guān)鍵字有如下三個作用:
·在工作內(nèi)存中,每次使用volatile修飾的變量前都必須先從主內(nèi)存刷新最新的值,用于保證能看見其他線程對volatile修飾的變量所做的修改后的值
·要求在工作內(nèi)存中,每次修改volatile修飾的變量后都必須立刻同步回主內(nèi)存中,用于保證其他線程可以看到自己對volatile修飾的變量所做的修改
·要求volatile修飾的變量不會被指令重排序優(yōu)化,保證代碼的執(zhí)行順序與程序的順序相同

先行發(fā)生原則
倘若在程序開發(fā)中,僅靠sychronized和volatile關(guān)鍵字來保證原子性、可見性以及有序性,那么編寫并發(fā)程序可能會顯得十分麻煩,幸 運的是,在Java內(nèi)存模型中,還提供了happens-before 原則來輔助保證程序執(zhí)行的原子性、可見性以及有序性的問題,它是判斷數(shù)據(jù)是否存在競爭、線程是否安全的依據(jù),happens-before 原則內(nèi)容如下
程序順序原則,即在一個線程內(nèi)必須保證語義串行性,也就是說按照代碼順序執(zhí)行。
(1)鎖規(guī)則:解鎖(unlock)操作必然發(fā)生在后續(xù)的同一個鎖的加鎖(lock)之前,也就是說,如果對于一個鎖解鎖后,再加鎖,那么加鎖的動作必須在解鎖動作之后(同一個鎖)。
(2)volatile規(guī)則:volatile變量的寫,先發(fā)生于讀,這保證了volatile變量的可見性,簡單的理解就是,volatile變量在每次被線程訪問時,都強迫從主內(nèi) 存中讀該變量的值,而當(dāng)該變量發(fā)生變化時,又會強迫將最新
的值刷新到主內(nèi)存,任何時刻,不同的線程總是能夠看到該變量的最新值。
(3)線程啟動規(guī)則:線程的start()方法先于它的每一個動作,即如果線程A在執(zhí)行線程B的start方法之前修改了共享變量的值,那么當(dāng)線程B執(zhí)行start方法時,線程A對共享變量的修改對線程B可見
(4)傳遞性:A先于B ,B先于C 那么A必然先于C
(5)線程終止規(guī)則 線程的所有操作先于線程的終結(jié),Thread.join()方法的作用是等待當(dāng)前執(zhí)行的線程終止。
(6)在線程B終止之前,修改了共享變量,線程A從線程B的join方法成功返回后,線程B對共享變量的修改將對線程A可見。
(7)線程中斷規(guī)則:對線程 interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過Thread.interrupted()方法檢測線程是否中斷。
(8)對象終結(jié)規(guī)則:對象的構(gòu)造函數(shù)執(zhí)行,結(jié)束先于finalize()方法
上述8條原則無需手動添加任何同步手段(synchronized|volatile)即可達(dá)到效果,下面我們結(jié)合前面的案例演示這8條原則如何判斷線程是否安全,如下:

class MixedOrder{
    int a = 0;
    boolean flag = false;
    public void writer(){
        a = 1;
       flag = true;
    }
    public void read(){
        if(flag){
        int i = a + 1;
        }
    }
}

同樣的道理,存在兩條線程A和B,線程A調(diào)用實例對象的writer()方法,而線程B調(diào)用實例對象的read()方法,線程A先啟動而線程B后啟動,那么線程B讀取到的i值是多少呢?現(xiàn)在依據(jù)8條原則,由于存在兩條線程同時調(diào)用,因此程序次序原則不合適。writer()方法和read()方法都沒有使用同步手段,鎖規(guī)則也不合適。沒有使用volatile關(guān)鍵字,volatile變量原則不適應(yīng)。線程啟動規(guī)則、線程終止規(guī)則、線程中斷規(guī)則、對象結(jié)規(guī)則、傳遞性和本次測試案例也不合適。線程A和線程B的啟動時間雖然有先后,但線程B執(zhí)行結(jié)果卻是不確定,也是說上述代碼沒有適合8條原則中的任意一,也沒有使用任何同步手段,所以上述的操作是線程不安全的,因此線程B讀取的值自然也是不確定的。修復(fù)這個問題的方式很簡單,要么給writer()方 法和read()方法添加同步手段,如synchronized或者給變量flag添加volatile關(guān)鍵字,確保線程A修改的值對線程B總是可見。

時間先后順序與先行發(fā)生原則之間基本沒有太大的關(guān)系,所以我們衡量并發(fā)安全問題的時候不要受到時間順序的干擾,一切必須以先行發(fā)生原則為準(zhǔn)。

猜你喜歡:


分享到:
在線咨詢 我要報名
和我們在線交談!