一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

(轉(zhuǎn)載)全面理解Java內(nèi)存模型(JMM)及volatile關(guān)鍵字

 昵稱60891057 2018-12-01

理解Java內(nèi)存區(qū)域與Java內(nèi)存模型

Java內(nèi)存區(qū)域

Java虛擬機(jī)在運(yùn)行程序時(shí)會(huì)把其自動(dòng)管理的內(nèi)存劃分為以上幾個(gè)區(qū)域,每個(gè)區(qū)域都有的用途以及創(chuàng)建銷毀的時(shí)機(jī),其中藍(lán)色部分代表的是所有線程共享的數(shù)據(jù)區(qū)域,而綠色部分代表的是每個(gè)線程的私有數(shù)據(jù)區(qū)域。

方法區(qū)(Method Area):

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

JVM堆(Java Heap):

Java 堆也是屬于線程共享的內(nèi)存區(qū)域,它在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建,是Java 虛擬機(jī)所管理的內(nèi)存中最大的一塊,主要用于存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存,注意Java 堆是垃圾收集器管理的主要區(qū)域,因此很多時(shí)候也被稱做GC 堆,如果在堆中沒(méi)有內(nèi)存完成實(shí)例分配,并且堆也無(wú)法再擴(kuò)展時(shí),將會(huì)拋出OutOfMemoryError 異常。

程序計(jì)數(shù)器(Program Counter Register):

屬于線程私有的數(shù)據(jù)區(qū)域,是一小塊內(nèi)存空間,主要代表當(dāng)前線程所執(zhí)行的字節(jié)碼行號(hào)指示器。字節(jié)碼解釋器工作時(shí),通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來(lái)完成。

虛擬機(jī)棧(Java Virtual Machine Stacks):

屬于線程私有的數(shù)據(jù)區(qū)域,與線程同時(shí)創(chuàng)建,總數(shù)與線程關(guān)聯(lián),代表Java方法執(zhí)行的內(nèi)存模型。每個(gè)方法執(zhí)行時(shí)都會(huì)創(chuàng)建一個(gè)棧楨來(lái)存儲(chǔ)方法的的變量表、操作數(shù)棧、動(dòng)態(tài)鏈接方法、返回值、返回地址等信息。每個(gè)方法從調(diào)用直結(jié)束就對(duì)于一個(gè)棧楨在虛擬機(jī)棧中的入棧和出棧過(guò)程,如下(圖有誤,應(yīng)該為棧楨):

本地方法棧(Native Method Stacks):

本地方法棧屬于線程私有的數(shù)據(jù)區(qū)域,這部分主要與虛擬機(jī)用到的 Native 方法相關(guān),一般情況下,我們無(wú)需關(guān)心此區(qū)域。

這里之所以簡(jiǎn)要說(shuō)明這部分內(nèi)容,注意是為了區(qū)別Java內(nèi)存模型與Java內(nèi)存區(qū)域的劃分,畢竟這兩種劃分是屬于不同層次的概念。

Java內(nèi)存模型概述

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)存中的變量副本拷貝,前面說(shuō)過(guò),工作內(nèi)存是每個(gè)線程的私有數(shù)據(jù)區(qū)域,因此不同的線程間無(wú)法訪問(wèn)對(duì)方的工作內(nèi)存,線程間的通信(傳值)必須通過(guò)主內(nèi)存來(lái)完成,其簡(jiǎn)要訪問(wèn)過(guò)程如下圖,

需要注意的是,JMM與Java內(nèi)存區(qū)域的劃分是不同的概念層次,更恰當(dāng)說(shuō)JMM描述的是一組規(guī)則,通過(guò)這組規(guī)則控制程序中各個(gè)變量在共享數(shù)據(jù)區(qū)域和私有數(shù)據(jù)區(qū)域的訪問(wèn)方式,JMM是圍繞原子性,有序性、可見(jiàn)性展開(kāi)的(稍后會(huì)分析)。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ī)棧以及本地方法棧?;蛟S在某些地方,我們可能會(huì)看見(jiàn)主內(nèi)存被描述為堆內(nèi)存,工作內(nèi)存被稱為線程棧,實(shí)際上他們表達(dá)的都是同一個(gè)含義。關(guān)于JMM中的主內(nèi)存和工作內(nèi)存說(shuō)明如下,

主內(nèi)存

主要存儲(chǔ)的是Java實(shí)例對(duì)象,所有線程創(chuàng)建的實(shí)例對(duì)象都存放在主內(nèi)存中,不管該實(shí)例對(duì)象是成員變量還是方法中的本地變量(也稱局部變量),當(dāng)然也包括了共享的類信息、常量、靜態(tài)變量。由于是共享數(shù)據(jù)區(qū)域,多條線程對(duì)同一個(gè)變量進(jìn)行訪問(wèn)可能會(huì)發(fā)現(xiàn)線程安全問(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)題。

弄清楚主內(nèi)存和工作內(nèi)存后,接了解一下主內(nèi)存與工作內(nèi)存的數(shù)據(jù)存儲(chǔ)類型以及操作方式,根據(jù)虛擬機(jī)規(guī)范,對(duì)于一個(gè)實(shí)例對(duì)象中的成員方法而言,如果方法中包含本地變量是基本數(shù)據(jù)類型(boolean,byte,short,char,int,long,float,double),將直接存儲(chǔ)在工作內(nèi)存的幀棧結(jié)構(gòu)中,但倘若本地變量是引用類型,那么該變量的引用會(huì)存儲(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è)方法,那么兩條線程會(huì)將要操作的數(shù)據(jù)拷貝一份到自己的工作內(nèi)存中,執(zhí)行完成操作后才刷新到主內(nèi)存,簡(jiǎn)單示意圖如下所示:

硬件內(nèi)存架構(gòu)與Java內(nèi)存模型

硬件內(nèi)存架構(gòu)

正如上圖所示,經(jīng)過(guò)簡(jiǎn)化CPU與內(nèi)存操作的簡(jiǎn)易圖,實(shí)際上沒(méi)有這么簡(jiǎn)單,這里為了理解方便,我們省去了南北橋并將三級(jí)緩存統(tǒng)一為CPU緩存(有些CPU只有二級(jí)緩存,有些CPU有三級(jí)緩存)。就目前計(jì)算機(jī)而言,一般擁有多個(gè)CPU并且每個(gè)CPU可能存在多個(gè)核心,多核是指在一枚處理器(CPU)中集成兩個(gè)或多個(gè)完整的計(jì)算引擎(內(nèi)核),這樣就可以支持多任務(wù)并行執(zhí)行,從多線程的調(diào)度來(lái)說(shuō),每個(gè)線程都會(huì)映射到各個(gè)CPU核心中并行運(yùn)行。在CPU內(nèi)部有一組CPU寄存器,寄存器是cpu直接訪問(wèn)和處理的數(shù)據(jù),是一個(gè)臨時(shí)放數(shù)據(jù)的空間。一般CPU都會(huì)從內(nèi)存取數(shù)據(jù)到寄存器,然后進(jìn)行處理,但由于內(nèi)存的處理速度遠(yuǎn)遠(yuǎn)低于CPU,導(dǎo)致CPU在處理指令時(shí)往往花費(fèi)很多時(shí)間在等待內(nèi)存做準(zhǔn)備工作,于是在寄存器和主內(nèi)存間添加了CPU緩存,CPU緩存比較小,但訪問(wèn)速度比主內(nèi)存快得多,如果CPU總是操作主內(nèi)存中的同一址地的數(shù)據(jù),很容易影響CPU執(zhí)行速度,此時(shí)CPU緩存就可以把從內(nèi)存提取的數(shù)據(jù)暫時(shí)保存起來(lái),如果寄存器要取內(nèi)存中同一位置的數(shù)據(jù),直接從緩存中提取,無(wú)需直接從主內(nèi)存取。需要注意的是,寄存器并不每次數(shù)據(jù)都可以從緩存中取得數(shù)據(jù),萬(wàn)一不是同一個(gè)內(nèi)存地址中的數(shù)據(jù),那寄存器還必須直接繞過(guò)緩存從內(nèi)存中取數(shù)據(jù)。所以并不每次都得到緩存中取數(shù)據(jù),這種現(xiàn)象有個(gè)專業(yè)的名稱叫做緩存的命中率,從緩存中取就命中,不從緩存中取從內(nèi)存中取,就沒(méi)命中,可見(jiàn)緩存命中率的高低也會(huì)影響CPU執(zhí)行性能,這就是CPU、緩存以及主內(nèi)存間的簡(jiǎn)要交互過(guò)程,總而言之當(dāng)一個(gè)CPU需要訪問(wèn)主存時(shí),會(huì)先讀取一部分主存數(shù)據(jù)到CPU緩存(當(dāng)然如果CPU緩存中存在需要的數(shù)據(jù)就會(huì)直接從緩存獲取),進(jìn)而在讀取CPU緩存到寄存器,當(dāng)CPU需要寫數(shù)據(jù)到主存時(shí),同樣會(huì)先刷新寄存器中的數(shù)據(jù)到CPU緩存,然后再把數(shù)據(jù)刷新到主內(nèi)存中。

Java線程與硬件處理器

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

如圖所示,每個(gè)線程最終都會(huì)映射到CPU中進(jìn)行處理,如果CPU存在多核,那么一個(gè)CPU將可以并行執(zhí)行多個(gè)線程任務(wù)。

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)镴MM只是一種抽象的概念,是一組規(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)存(有些地方稱為??臻g),用于存儲(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)題。如下圖,主內(nèi)存中存在一個(gè)共享變量x,現(xiàn)在有A和B兩條線程分別對(duì)該變量x=1進(jìn)行操作,A/B線程各自的工作內(nèi)存中存在共享變量副本x。假設(shè)現(xiàn)在A線程想要修改x的值為2,而B(niǎo)線程卻想要讀取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)題,假如A線程修改完后正在將數(shù)據(jù)寫回主內(nèi)存,而B(niǎo)線程此時(shí)正在讀取主內(nèi)存,即將x=1拷貝到自己的工作內(nèi)存中,這樣B線程讀取到的值就是x=1,但如果A線程已將x=2寫回主內(nèi)存后,B線程才開(kāi)始讀取的話,那么此時(shí)B線程讀取到的就是x=2,但到底是哪種情況先發(fā)生呢?這是不確定的,這也就是所謂的線程安全問(wèn)題。 

為了解決類似上述的問(wèn)題,JVM定義了一組規(guī)則,通過(guò)這組規(guī)則來(lái)決定一個(gè)線程對(duì)共享變量的寫入何時(shí)對(duì)另一個(gè)線程可見(jiàn),這組規(guī)則也稱為Java內(nèi)存模型(即JMM),JMM是圍繞著程序執(zhí)行的原子性、有序性、可見(jiàn)性展開(kāi)的,下面我們看看這三個(gè)特性。

Java內(nèi)存模型的承諾

這里我們先來(lái)了解幾個(gè)概念,即原子性?可見(jiàn)性?有序性?最后再闡明JMM是如何保證這3個(gè)特性。

原子性

原子性指的是一個(gè)操作是不可中斷的,即使是在多線程環(huán)境下,一個(gè)操作一旦開(kāi)始就不會(huì)被其他線程影響。比如對(duì)于一個(gè)靜態(tài)變量int x,兩條線程同時(shí)對(duì)他賦值,線程A賦值為1,而線程B賦值為2,不管線程如何運(yùn)行,最終x的值要么是1,要么是2,線程A和線程B間的操作是沒(méi)有干擾的,這就是原子性操作,不可被中斷的特點(diǎn)。有點(diǎn)要注意的是,對(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ǔ)單元,這樣會(huì)導(dǎo)致一個(gè)線程在寫時(shí),操作完前32位的原子操作后,輪到B線程讀取時(shí),恰好只讀取到了后32位的數(shù)據(jù),這樣可能會(huì)讀取到一個(gè)既非原值又不是線程修改值的變量,它可能是“半個(gè)變量”的數(shù)值,即64位數(shù)據(jù)被兩個(gè)線程分成了兩次讀取。但也不必太擔(dān)心,因?yàn)樽x取到“半個(gè)變量”的情況比較少見(jiàn),至少在目前的商用的虛擬機(jī)中,幾乎都把64位的數(shù)據(jù)的讀寫操作作為原子操作來(lái)執(zhí)行,因此對(duì)于這個(gè)問(wèn)題不必太在意,知道這么回事即可。

理解指令重排

計(jì)算機(jī)在執(zhí)行程序時(shí),為了提高性能,編譯器和處理器的常常會(huì)對(duì)指令做重排,一般分以下3種

編譯器優(yōu)化的重排

編譯器在不改變單線程程序語(yǔ)義的前提下,可以重新安排語(yǔ)句的執(zhí)行順序。

指令并行的重排

現(xiàn)代處理器采用了指令級(jí)并行技術(shù)來(lái)將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性(即后一個(gè)執(zhí)行的語(yǔ)句無(wú)需依賴前面執(zhí)行的語(yǔ)句的結(jié)果),處理器可以改變語(yǔ)句對(duì)應(yīng)的機(jī)器指令的執(zhí)行順序。

內(nèi)存系統(tǒng)的重排

由于處理器使用緩存和讀寫緩存沖區(qū),這使得加載(load)和存儲(chǔ)(store)操作看上去可能是在亂序執(zhí)行,因?yàn)槿?jí)緩存的存在,導(dǎo)致內(nèi)存與緩存的數(shù)據(jù)同步存在時(shí)間差。

其中編譯器優(yōu)化的重排屬于編譯期重排,指令并行的重排和內(nèi)存系統(tǒng)的重排屬于處理器重排,在多線程環(huán)境中,這些重排優(yōu)化可能會(huì)導(dǎo)致程序出現(xiàn)內(nèi)存可見(jiàn)性問(wèn)題,下面分別闡明這兩種重排優(yōu)化可能帶來(lái)的問(wèn)題。

編譯器重排

下面我們簡(jiǎn)單看一個(gè)編譯器重排的例子:

線程 1 線程 2

1:x2 = a ; 3: x1 = b ;

2: b = 1; 4: a = 2 ;

兩個(gè)線程同時(shí)執(zhí)行,分別有1、2、3、4四段執(zhí)行代碼,其中1、2屬于線程1 , 3、4屬于線程2 ,從程序的執(zhí)行順序上看,似乎不太可能出現(xiàn)x1 = 1 和x2 = 2 的情況,但實(shí)際上這種情況是有可能發(fā)現(xiàn)的,因?yàn)槿绻幾g器對(duì)這段程序代碼執(zhí)行重排優(yōu)化后,可能出現(xiàn)下列情況,

線程 1 線程 2

2: b = 1; 4: a = 2 ; 

1:x2 = a ; 3: x1 = b ;

這種執(zhí)行順序下就有可能出現(xiàn)x1 = 1 和x2 = 2 的情況,這也就說(shuō)明在多線程環(huán)境下,由于編譯器優(yōu)化重排的存在,兩個(gè)線程中使用的變量能否保證一致性是無(wú)法確定的。

處理器指令重排

先了解一下指令重排的概念,處理器指令重排是對(duì)CPU的性能優(yōu)化,從指令的執(zhí)行角度來(lái)說(shuō)一條指令可以分為多個(gè)步驟完成,如下,

1. 取指 IF

2. 譯碼和取寄存器操作數(shù) ID

3. 執(zhí)行或者有效地址計(jì)算 EX

4. 存儲(chǔ)器訪問(wèn) MEM

5. 寫回 WB

CPU在工作時(shí),需要將上述指令分為多個(gè)步驟依次執(zhí)行(注意硬件不同有可能不一樣),由于每一個(gè)步會(huì)使用到不同的硬件操作,比如取指時(shí)會(huì)只有PC寄存器和存儲(chǔ)器,譯碼時(shí)會(huì)執(zhí)行到指令寄存器組,執(zhí)行時(shí)會(huì)執(zhí)行ALU(算術(shù)邏輯單元)、寫回時(shí)使用到寄存器組。為了提高硬件利用率,CPU指令是按流水線技術(shù)來(lái)執(zhí)行的,如下:

從圖中可以看出當(dāng)指令1還未執(zhí)行完成時(shí),第2條指令便利用空閑的硬件開(kāi)始執(zhí)行,這樣做是有好處的,如果每個(gè)步驟花費(fèi)1ms,那么如果第2條指令需要等待第1條指令執(zhí)行完成后再執(zhí)行的話,則需要等待5ms,但如果使用流水線技術(shù)的話,指令2只需等待1ms就可以開(kāi)始執(zhí)行了,這樣就能大大提升CPU的執(zhí)行性能。雖然流水線技術(shù)可以大大提升CPU的性能,但不幸的是一旦出現(xiàn)流水中斷,所有硬件設(shè)備將會(huì)進(jìn)入一輪停頓期,當(dāng)再次彌補(bǔ)中斷點(diǎn)可能需要幾個(gè)周期,這樣性能損失也會(huì)很大,就好比工廠組裝手機(jī)的流水線,一旦某個(gè)零件組裝中斷,那么該零件往后的工人都有可能進(jìn)入一輪或者幾輪等待組裝零件的過(guò)程。因此我們需要盡量阻止指令中斷的情況,指令重排就是其中一種優(yōu)化中斷的手段,我們通過(guò)一個(gè)例子來(lái)闡明指令重排是如何阻止流水線技術(shù)中斷的,

a = b + c ;

d = e + f ;

下面通過(guò)匯編指令展示了上述代碼在CPU執(zhí)行的處理過(guò)程,

LW指令 表示 load,其中LW R1,b表示把b的值加載到寄存器R1中

LW R2,c 表示把c的值加載到寄存器R2中

ADD 指令表示加法,把R1 、R2的值相加,并存入R3寄存器中。

SW 表示 store 即將 R3寄存器的值保持到變量a中

LW R4,e 表示把e的值加載到寄存器R4中

LW R5,f 表示把f的值加載到寄存器R5中

SUB 指令表示減法,把R4 、R5的值相減,并存入R6寄存器中。

SW d,R6 表示將R6寄存器的值保持到變量d中

上述便是匯編指令的執(zhí)行過(guò)程,在某些指令上存在X的標(biāo)志,X代表中斷的含義,也就是只要有X的地方就會(huì)導(dǎo)致指令流水線技術(shù)停頓,同時(shí)也會(huì)影響后續(xù)指令的執(zhí)行,可能需要經(jīng)過(guò)1個(gè)或幾個(gè)指令周期才可能恢復(fù)正常,那為什么停頓呢?這是因?yàn)椴糠謹(jǐn)?shù)據(jù)還沒(méi)準(zhǔn)備好,如執(zhí)行ADD指令時(shí),需要使用到前面指令的數(shù)據(jù)R1,R2,而此時(shí)R2的MEM操作沒(méi)有完成,即未拷貝到存儲(chǔ)器中,這樣加法計(jì)算就無(wú)法進(jìn)行,必須等到MEM操作完成后才能執(zhí)行,也就因此而停頓了,其他指令也是類似的情況。前面闡述過(guò),停頓會(huì)造成CPU性能下降,因此我們應(yīng)該想辦法消除這些停頓,這時(shí)就需要使用到指令重排了,如下圖,既然ADD指令需要等待,那我們就利用等待的時(shí)間做些別的事情,如把LW R4,e 和 LW R5,f 移動(dòng)到前面執(zhí)行,畢竟LW R4,e 和 LW R5,f執(zhí)行并沒(méi)有數(shù)據(jù)依賴關(guān)系,對(duì)他們有數(shù)據(jù)依賴關(guān)系的SUB R6,R5,R4指令在R4,R5加載完成后才執(zhí)行的,沒(méi)有影響,過(guò)程如下:


正如上圖所示,所有的停頓都完美消除了,指令流水線也無(wú)需中斷了,這樣CPU的性能也能帶來(lái)很好的提升,這就是處理器指令重排的作用。關(guān)于編譯器重排以及指令重排(這兩種重排我們后面統(tǒng)一稱為指令重排)相關(guān)內(nèi)容已闡述清晰了,我們必須意識(shí)到對(duì)于單線程而已指令重排幾乎不會(huì)帶來(lái)任何影響,比竟重排的前提是保證串行語(yǔ)義執(zhí)行的一致性,但對(duì)于多線程環(huán)境而已,指令重排就可能導(dǎo)致嚴(yán)重的程序輪序執(zhí)行問(wèn)題,如下,

class MixedOrder {

    int a = 0;

    boolean flag = false;

    public void writer() {

        a = 1;

        flag = true;

    }

    public void read(){

        if(flag){

            int i = a + 1;

        }

    }

}

如上述代碼,同時(shí)存在線程A和線程B對(duì)該實(shí)例對(duì)象進(jìn)行操作,其中A線程調(diào)用寫入方法,而B(niǎo)線程調(diào)用讀取方法,由于指令重排等原因,可能導(dǎo)致程序執(zhí)行順序變?yōu)槿缦拢?/p>

線程A 線程B

 writer: read:

1:flag = true;          1:flag = true;

2:a = 1;                   2: a = 0 ; //誤讀

                               3: i = 1 ;

由于指令重排的原因,線程A的flag置為true被提前執(zhí)行了,而a賦值為1的程序還未執(zhí)行完,此時(shí)線程B,恰好讀取flag的值為true,直接獲取a的值(此時(shí)B線程并不知道a為0)并執(zhí)行i賦值操作,結(jié)果i的值為1,而不是預(yù)期的2,這就是多線程環(huán)境下,指令重排導(dǎo)致的程序亂序執(zhí)行的結(jié)果。因此,請(qǐng)記住,指令重排只會(huì)保證單線程中串行語(yǔ)義的執(zhí)行的一致性,但并不會(huì)關(guān)心多線程間的語(yǔ)義一致性。

可見(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)象就造成了可見(jiàn)性問(wèn)題,另外指令重排以及編譯器優(yōu)化也可能導(dǎo)致可見(jiàn)性問(wèn)題,通過(guò)前面的分析,我們知道無(wú)論是編譯器優(yōu)化還是處理器優(yōu)化的重排現(xiàn)象,在多線程環(huán)境下,確實(shí)會(huì)導(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ī)器碼指令后可能會(huì)出現(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)題后,看看JMM是如何保證的,在Java內(nèi)存模型中都提供一套解決方案供Java工程師在開(kāi)發(fā)過(guò)程使用,如原子性問(wèn)題,除了JVM自身提供的對(duì)基本數(shù)據(jù)類型讀寫操作的原子性外,對(duì)于方法級(jí)別或者代碼塊級(jí)別的原子性操作,可以使用synchronized關(guān)鍵字或者重入鎖(ReentrantLock)保證程序執(zhí)行的原子性,關(guān)于synchronized的詳解,看博主另外一篇文章( 深入理解Java并發(fā)之synchronized實(shí)現(xiàn)原理)。而工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象導(dǎo)致的可見(jiàn)性問(wèn)題,可以使用synchronized關(guān)鍵字或者volatile關(guān)鍵字解決,它們都可以使一個(gè)線程修改后的變量立即對(duì)其他線程可見(jiàn)。對(duì)于指令重排導(dǎo)致的可見(jiàn)性問(wèn)題和有序性問(wèn)題,則可以利用volatile關(guān)鍵字解決,因?yàn)関olatile的另外一個(gè)作用就是禁止重排序優(yōu)化,關(guān)于volatile稍后會(huì)進(jìn)一步分析。除了靠sychronized和volatile關(guān)鍵字來(lái)保證原子性、可見(jiàn)性以及有序性外,JMM內(nèi)部還定義一套happens-before 原則來(lái)保證多線程環(huán)境下兩個(gè)操作間的原子性、可見(jiàn)性以及有序性。

理解JMM中的happens-before 原則

倘若在程序開(kāi)發(fā)中,僅靠sychronized和volatile關(guān)鍵字來(lái)保證原子性、可見(jiàn)性以及有序性,那么編寫并發(fā)程序可能會(huì)顯得十分麻煩,幸運(yùn)的是,在Java內(nèi)存模型中,還提供了happens-before 原則來(lái)輔助保證程序執(zhí)行的原子性、可見(jiàn)性以及有序性的問(wèn)題,它是判斷數(shù)據(jù)是否存在競(jìng)爭(zhēng)、線程是否安全的依據(jù),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

線程終止規(guī)則 線程的所有操作先于線程的終結(jié),Thread.join()方法的作用是等待當(dāng)前執(zhí)行的線程終止。假設(shè)在線程B終止之前,修改了共享變量,線程A從線程B的join方法成功返回后,線程B對(duì)共享變量的修改將對(duì)線程A可見(jiàn)。

線程中斷規(guī)則 對(duì)線程 interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生,可以通過(guò)Thread.interrupted()方法檢測(cè)線程是否中斷。

對(duì)象終結(jié)規(guī)則 對(duì)象的構(gòu)造函數(shù)執(zhí)行,結(jié)束先于finalize()方法

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

volatile內(nèi)存語(yǔ)義

volatile在并發(fā)編程中很常見(jiàn),但也容易被濫用,現(xiàn)在我們就進(jìn)一步分析volatile關(guān)鍵字的語(yǔ)義。volatile是Java虛擬機(jī)提供的輕量級(jí)的同步機(jī)制。volatile關(guān)鍵字有如下兩個(gè)作用:

1. 保證被volatile修飾的共享變量對(duì)所有線程總數(shù)可見(jiàn)的,也就是當(dāng)一個(gè)線程修改了一個(gè)被volatile修飾共享變量的值,新值總數(shù)可以被其他線程立即得知。

2. 禁止指令重排序優(yōu)化。

volatile的可見(jiàn)性

關(guān)于volatile的可見(jiàn)性作用,我們必須意識(shí)到被volatile修飾的變量對(duì)所有線程總數(shù)立即可見(jiàn)的,對(duì)volatile變量的所有寫操作總是能立刻反應(yīng)到其他線程中,但是對(duì)于volatile變量運(yùn)算操作在多線程環(huán)境并不保證安全性,如下,

public class VolatileVisibility {

    public static volatile int i =0;

    public static void increase(){

        i++;

    }

}

正如上述代碼所示,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修飾方法后,由于synchronized本身也具備與volatile相同的特性,即可見(jiàn)性,因此在這樣種情況下就完全可以省去volatile修飾變量。

public class VolatileVisibility {

    public static int i =0;

    public synchronized static void increase(){

        i++;

    }

}

現(xiàn)在來(lái)看另外一種場(chǎng)景,可以使用volatile修飾變量達(dá)到線程安全的目的,如下,

public class VolatileSafe {

    volatile boolean close;

    public void close() {

        close=true;

    }

    public void doWork(){

        while (!close) {

            System.out.println('safe....');

        }

    }

}

由于對(duì)于boolean變量close值的修改屬于原子性操作,因此可以通過(guò)使用volatile修飾變量close,使用該變量對(duì)其他線程立即可見(jiàn),從而達(dá)到線程安全的目的。那么JMM是如何實(shí)現(xiàn)讓volatile變量對(duì)其他線程立即可見(jiàn)的呢?實(shí)際上,當(dāng)寫一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的工作內(nèi)存中的共享變量值刷新到主內(nèi)存中,當(dāng)讀取一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的工作內(nèi)存置為無(wú)效,那么該線程將只能從主內(nèi)存中重新讀取共享變量。volatile變量正是通過(guò)這種寫-讀方式實(shí)現(xiàn)對(duì)其他線程可見(jiàn)(但其內(nèi)存語(yǔ)義實(shí)現(xiàn)則是通過(guò)內(nèi)存屏障,稍后會(huì)說(shuō)明)。

volatile禁止重排優(yōu)化

volatile關(guān)鍵字另一個(gè)作用就是禁止指令重排優(yōu)化,從而避免多線程環(huán)境下程序出現(xiàn)亂序執(zhí)行的現(xiàn)象,關(guān)于指令重排優(yōu)化前面已詳細(xì)分析過(guò),這里主要簡(jiǎn)單說(shuō)明一下volatile是如何實(shí)現(xiàn)禁止指令重排優(yōu)化的。先了解一個(gè)概念,內(nèi)存屏障(Memory Barrier)。 

內(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,如下:

/**

* Created by zejian on 2017/6/11.

* Blog : http://blog.csdn.net/javazejian [原文地址,請(qǐng)尊重原創(chuàng)]

*/public class DoubleCheckLock {

    private 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)境下就可以出現(xiàn)線程安全問(wèn)題。原因在于某一個(gè)線程執(zhí)行到第一次檢測(cè),讀取到的instance不為null時(shí),instance的引用對(duì)象可能沒(méi)有完成初始化。因?yàn)閕nstance = 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ú)論重排前還是重排后程序的執(zhí)行結(jié)果在單線程中并沒(méi)有改變,因此這種重排優(yōu)化是允許的。但是指令重排只會(huì)保證串行語(yǔ)義的執(zhí)行的一致性(單線程),但并不會(huì)關(guān)心多線程間的語(yǔ)義一致性。所以當(dāng)一條線程訪問(wèn)instance不為null時(shí),由于instance實(shí)例未必已初始化完成,也就造成了線程安全問(wèn)題。那么該如何解決呢,很簡(jiǎn)單,我們使用volatile禁止instance變量被執(zhí)行指令重排優(yōu)化即可。

//禁止指令重排優(yōu)化

private volatile static DoubleCheckLock instance;

ok~,到此相信我們對(duì)Java內(nèi)存模型和volatile應(yīng)該都有了比較全面的認(rèn)識(shí),總而言之,我們應(yīng)該清楚知道,JMM就是一組規(guī)則,這組規(guī)則意在解決在并發(fā)編程可能出現(xiàn)的線程安全問(wèn)題,并提供了內(nèi)置解決方案(happen-before原則)及其外部可使用的同步手段(synchronized/volatile等),確保了程序執(zhí)行在多線程環(huán)境中的應(yīng)有的原子性,可視性及其有序性。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多

    亚洲欧美日韩中文字幕二欧美| 欧美欧美日韩综合一区| 亚洲天堂久久精品成人| 婷婷九月在线中文字幕| 精品国模一区二区三区欧美| 日本一本不卡免费视频| 日韩精品一区二区三区av在线| 中文字幕一区久久综合| 国产精品亚洲精品亚洲| 粗暴蹂躏中文一区二区三区| 国产在线一区二区免费| 欧美日韩国产自拍亚洲| 色婷婷日本视频在线观看| 欧美一级内射一色桃子| 久久精品一区二区少妇| 搡老妇女老熟女一区二区| 99精品国产一区二区青青 | 在线观看日韩欧美综合黄片| 搡老熟女老女人一区二区| 人体偷拍一区二区三区| 精品人妻一区二区三区四区久久| 欧美日韩有码一二三区| 日韩欧美三级中文字幕| 日本午夜免费啪视频在线| 久热这里只有精品九九| 欧美野外在线刺激在线观看| 亚洲高清亚洲欧美一区二区| 国语久精品在视频在线观看| 欧美国产极品一区二区| 日韩精品一区二区三区四区| 欧美在线观看视频免费不卡| 国产内射一级一片内射高清视频| 国产国产精品精品在线| 国产又粗又猛又爽又黄的文字| 亚洲综合香蕉在线视频| 成在线人免费视频一区二区| 亚洲精品有码中文字幕在线观看 | 国产高清三级视频在线观看| 欧美加勒比一区二区三区| 亚洲国产精品无遮挡羞羞| 日本精品视频一二三区|