深入理解JVM第十二章筆記

深入理解JVM第十二章筆記

背景

為了充分壓榨計算機(jī)處理器的性能,多任務(wù)處理在現(xiàn)代計算機(jī)操作系統(tǒng)中已經(jīng)是一項必備技能了。

另外由于大部分的計算任務(wù)都不可能只靠處理器來單獨“計算”完成,處理器需要與內(nèi)存交互,如讀取運(yùn)算數(shù)據(jù),存儲運(yùn)算結(jié)果等,這個IO操作是很難消除的。而如今的計算機(jī)的存儲設(shè)備與處理器的運(yùn)算速度有幾個數(shù)量級的差距,所以需要在處理器與內(nèi)存之間加入一層---高速緩存,用來將運(yùn)算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算能快速進(jìn)行,當(dāng)運(yùn)算結(jié)束后再從緩存同步回內(nèi)存之中,這樣處理器就可以無需等待緩慢的內(nèi)存讀寫了。

高速緩存的引入也帶來了一個新的問題:緩存一致性:

多處理器系統(tǒng),每個處理器都有自己的高速緩存,它們又共享同一個主存:

圖1.png

當(dāng)多個處理器的運(yùn)算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時,將可能導(dǎo)致各自的緩存數(shù)據(jù)不一致

為了解決一致性問題,需要各個處理器訪問緩存時都遵循一些協(xié)議,在讀寫時根據(jù)協(xié)議來進(jìn)行操作,這類協(xié)議有:MSI,MESI等等

所謂的“內(nèi)存模型”,可以理解為在特定的操作協(xié)議下,對特定的內(nèi)存或高速緩存進(jìn)行讀寫訪問的過程抽象,不同的物理機(jī)器有不一樣的內(nèi)存模型,JVM也有屬于自己的內(nèi)存模型。

Java內(nèi)存模型

Java虛擬機(jī)規(guī)范定義了一種Java內(nèi)存模型(JMM)來屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實現(xiàn)讓Java程序在各種平臺下都能達(dá)到一致的內(nèi)存訪問效果。

主內(nèi)存與工作內(nèi)存

JMM的主要目標(biāo)是定義程序中各個變量的訪問規(guī)則,即在虛擬機(jī)中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié),此處的變量與Java編程中所說的變量有所區(qū)別,它包括:

  • 實例字段

  • 靜態(tài)字段

  • 構(gòu)成數(shù)組的元素

但不包括局部變量與方法參數(shù),因為它們屬于線程私有的。

JMM規(guī)定了所有變量都存儲在主內(nèi)存中,每條線程還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了被該線程使用到的變量的主內(nèi)存副本拷貝,線性對變量的所有操作(讀取,賦值等)都必須在工作內(nèi)存進(jìn)行,不能直接讀寫主內(nèi)存中的變量

線程,主內(nèi)存,工作內(nèi)存三者的交互關(guān)系:

圖2.png

內(nèi)存間交互操作

對于主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存,如何從工作內(nèi)存同步回主內(nèi)存之類的實現(xiàn)細(xì)節(jié),Java內(nèi)存模型定義了8種操作來完成主內(nèi)存與工作內(nèi)存的讀寫交互,虛擬機(jī)實現(xiàn)保證每一種操作都是原子的,不可再分的

  • Lock鎖定 作用于主內(nèi)存變量,將變量標(biāo)志為一條線程所獨占
  • Unlock解鎖 作用于主內(nèi)存變量,將處于鎖定的變量釋放出來
  • Read讀取 作用于主內(nèi)存變量,它將一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中
  • Load載入 作用于工作內(nèi)存變量,它把從主內(nèi)存讀取的變量值放入工作內(nèi)存的副本中
  • Use使用 作用于工作內(nèi)存變量,將工作內(nèi)存變量值傳遞給執(zhí)行引擎
  • Assgin賦值 作用于工作內(nèi)存變量,將執(zhí)行引擎的值傳遞給工作內(nèi)存的變量
  • Store存儲 作用于工作內(nèi)存變量,它把工作內(nèi)存變量傳遞到主內(nèi)存中
  • 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)存模型這2個操作必須順序執(zhí)行,但不保證連續(xù)執(zhí)行,即在指令之間可以插入其它指令

但是Java內(nèi)存模型規(guī)定了一些必要的規(guī)則

  • 不允許read load store write單獨出現(xiàn),即不允許一個變量讀取到工作內(nèi)存,但沒有變量接收的情況
  • 不允許一個線程丟棄它的assign操作,即變量在工作內(nèi)存改變必須同步回主內(nèi)存
  • 不允許一個線程無原因(沒有發(fā)生assgin賦值操作)把數(shù)據(jù)從線程的工作內(nèi)存同步會主內(nèi)存
  • 一個新的變量只能在主內(nèi)存中誕生,不允許在工作內(nèi)存中直接使用未被初始化的變量
  • 一個變量同一時刻只允許一條線程對其進(jìn)行Lock鎖定,但Lock操作可以被同一線程重復(fù)執(zhí)行
  • 如果對一個變量執(zhí)行Lock鎖定,會清空工作內(nèi)存中該副本的值,即執(zhí)行引擎使用該值會重新load assgin操作初始化該值
  • 如果一個變量事先沒有被Lock鎖定,那就不允許進(jìn)行Unlock操作,也不允許Unlock其它線程鎖定的變量

  • 對一個變量執(zhí)行Unlock操作,必須先把此變量值同步回主內(nèi)存(store write操作)

對于volatile型變量的特殊規(guī)則

當(dāng)一個變量定義為volatile之后,它將具備兩種特性:

  • 保證此變量對所有線程的可見性,這里的“可見性”是指當(dāng)一條線程修改了這個變量的值,新值對于其他線程來說是可以立即得知的,而普通變量不能做到這一點。普通變量的值在線程間的傳遞需要通過主存來完成:線程A修改一個普通變量的值,然后向主內(nèi)存進(jìn)行回寫,另外一條線程B在線程A回寫完成之后再從主內(nèi)存進(jìn)行讀取操作,新變量值才會對線程B可見
    但是volatile 不能保證線程是安全的,因為java里面的運(yùn)算并非原子操作

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

原子性 可見性 有序性

  • 原子性

大致可以認(rèn)為基本數(shù)據(jù)類型的訪問讀寫是具備原子性的

  • 可見性

當(dāng)一個線程修改了共享變量的值,其他線程能夠立即得知這個修改

除了volatile,能夠保證可見性的還有:

  • synchronized

  • final

  • 有序性

Java提供了volatile和synchronized保證有序性

先行發(fā)生原則

先行發(fā)生是指JMM中定義的兩項操作直接的偏序關(guān)系,如果說操作A先行發(fā)生于操作B,其實就是說在發(fā)生操作B之前,操作A產(chǎn)生的影響能被操作B觀察到,“影響”包括了:

  • 修改了內(nèi)存中共享變量的值

  • 發(fā)送了消息

  • 調(diào)用了方法

等等

例子:



i = 1 // 線程A中執(zhí)行


j = i // 線程B執(zhí)行

i = 2 // 線程C執(zhí)行

假設(shè)A中操作" i = 1"先行發(fā)生于B的操作" j = i",那么可以確定在B的操作執(zhí)行后,變量j的值一定等于1,得出這個結(jié)論的依據(jù):

  • 根據(jù)先行發(fā)生原則,“ i=1”的結(jié)果可以被觀察到

  • C還沒考慮進(jìn)來,A操作結(jié)束之后沒有線程會修改i的值了

現(xiàn)在把C考慮進(jìn)去:

依舊保持A和B之間的先行發(fā)生關(guān)系,C出現(xiàn)在A和B之間,但C和B沒有先行發(fā)生關(guān)系,那j的值就會出現(xiàn)不確定的情況,1或2都有可能:因為C對變量i的影響可能會被B觀察到,也可能不會

下面是JMM中一些“天然的”先行發(fā)生關(guān)系:

  • 程序次序規(guī)則:

在一個線程內(nèi),按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。準(zhǔn)確地說是控制流順序而不是程序代碼順序,因為要考慮分支、循環(huán)等結(jié)構(gòu)。

  • 管程鎖定規(guī)則

一個unlock操作先行發(fā)生于后面對同一個鎖的lock操作。這里必須強(qiáng)調(diào)的是同一個鎖,這里的“后面”指的是時間上的先后順序。

  • volatile變量規(guī)則

對一個volatile變量的寫操作先行發(fā)生于后面對這個變量的讀操作,這里的“后面”指的是時間上的先后順序

  • 線程啟動規(guī)則

Thread對象的start()方法先行發(fā)生于此線程的每一個動作。

  • 線程終止規(guī)則

線程中的所有操作都先行發(fā)生于對此線程的終止檢測,我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值等手段檢測到線程已經(jīng)終止執(zhí)行。

  • 線程中斷規(guī)則

對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生(即先中斷,后發(fā)現(xiàn)被中斷),可以通過Thread.interrupted()方法檢測到是否有中斷發(fā)生。

  • 對象終結(jié)規(guī)則

一個對象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的finalize()方法的開始。

  • 傳遞性

若操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那么就可以得出操作A先行發(fā)生于操作C。

以上規(guī)則是Java語言“天然”存在的規(guī)則,無需同步手段(例:加synchronized)就能保證先行發(fā)生。

例子:



  private int value = 0;


    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

假設(shè):

有線程A和B,A先調(diào)用了setValue(1),然后B調(diào)用了getValue()

那B收到的返回值是什么?

依次分析先行發(fā)生原則中的各項規(guī)則:

  • 兩個方法分別由兩個線程調(diào)用,不在一個線程中,所以程序次序規(guī)則在這里不適用
  • 沒有同步塊,自然不會發(fā)生lock和unlock操作,管程鎖定規(guī)則不適用

  • value變量沒有volatile修飾,volatile變量規(guī)則不適用

  • 后續(xù)的線程啟動,中止,中斷規(guī)則和對象終結(jié)規(guī)則也和這里無關(guān)系

沒有一個適用的先行發(fā)生原則,傳遞性也無從談起,所以這里的操作不是線程安全的

Java與線程

線程的實現(xiàn)

線程的引入可以把一個進(jìn)程的資源分配和執(zhí)行調(diào)度分開,線程既可共享進(jìn)程資源(內(nèi)存地址、文件I/O等),也可獨立調(diào)度(線程是CPU調(diào)度的基本單位)

實現(xiàn)線程有三種方式:

  • 1 使用內(nèi)核線程實現(xiàn)

內(nèi)核線程(Kernel-Level Thread,KLT)就是直接由操作系統(tǒng)內(nèi)核(Kernel,下稱內(nèi)核)支持的線程,這種線
圖3.png

程由內(nèi)核來完成線程切換,內(nèi)核通過操縱調(diào)度器(Scheduler)對線程進(jìn)行調(diào)度,并負(fù)責(zé)將線程的任務(wù)映射到各個處理器上。 每個內(nèi)核線程可以視為內(nèi)核的一個分身,這樣操作系統(tǒng)就有能力同時處理多件事情,支持多線程的內(nèi)核就叫做多線程內(nèi)核(MultiThreads Kernel)。

程序一般不會直接去使用內(nèi)核線程,而是去使用內(nèi)核線程的一種高級接口——輕量級進(jìn)程(Light Weight Process,LWP),輕量級進(jìn)程就是我們通常意義上所講的線程,由于每個輕量級進(jìn)程都由一個內(nèi)核線程支持,因此只有先支持內(nèi)核線程,才能有輕量級進(jìn)程。 這種輕量級進(jìn)程與內(nèi)核線程之間1:1的關(guān)系稱為一對一的線程模型,如下圖所示:

由于內(nèi)核線程的支持,每個輕量級進(jìn)程都成為一個獨立的調(diào)度單元,即使有一個輕量級進(jìn)程在系統(tǒng)調(diào)用中阻塞了,也不會影響整個進(jìn)程繼續(xù)工作,但是輕量級進(jìn)程具有它的局限性:首先,由于是基于內(nèi)核線程實現(xiàn)的,所以各種線程操作,如創(chuàng)建、 析構(gòu)及同步,都需要進(jìn)行系統(tǒng)調(diào)用。 而系統(tǒng)調(diào)用的代價相對較高,需要在用戶態(tài)(User Mode)和內(nèi)核態(tài)(Kernel Mode)中來回切換。 其次,每個輕量級進(jìn)程都需要有一個內(nèi)核線程的支持,因此輕量級進(jìn)程要消耗一定的內(nèi)核資源(如內(nèi)核線程的??臻g),因此一個系統(tǒng)支持輕量級進(jìn)程的數(shù)量是有限的。

  • 2 使用用戶線程實現(xiàn)

從廣義上來講,一個線程只要不是內(nèi)核線程,就可以認(rèn)為是用戶線程(UserThread,UT),因此,從這個定義上來講,輕量級進(jìn)程也屬于用戶線程,但輕量級進(jìn)程的實現(xiàn)始終是建立在內(nèi)核之上的,許多操作都要進(jìn)行系統(tǒng)調(diào)用,效率會受到限制。

而狹義上的用戶線程指的是完全建立在用戶空間的線程庫上,系統(tǒng)內(nèi)核不能感知線程存在的實現(xiàn)。 用戶線程的建立、 同步、 銷毀和調(diào)度完全在用戶態(tài)中完成,不需要內(nèi)核的幫助。如果程序?qū)崿F(xiàn)得當(dāng),這種線程不需要切換到內(nèi)核態(tài),因此操作可以是非??焖偾业拖牡模部梢灾С忠?guī)模更大的線程數(shù)量,部分高性能數(shù)據(jù)庫中的多線程就是由用戶線程實現(xiàn)的。 這種進(jìn)程與用戶線程之間1:N的關(guān)系稱為一對多的線程模型,如下圖所示:

圖4.png

使用用戶線程的優(yōu)勢在于不需要系統(tǒng)內(nèi)核支援,劣勢也在于沒有系統(tǒng)內(nèi)核的支援,所有的線程操作都需要用戶程序自己處理。 線程的創(chuàng)建、 切換和調(diào)度都是需要考慮的問題,而且由于操作系統(tǒng)只把處理器資源分配到進(jìn)程,那諸如“阻塞如何處理”、 “多處理器系統(tǒng)中如何將線程映射到其他處理器上”這類問題解決起來將會異常困難,甚至不可能完成。 因而使用用戶線程實現(xiàn)的程序一般都比較復(fù)雜,除了以前在不支持多線程的操作系統(tǒng)中(如DOS)的多線程程序與少數(shù)有特殊需求的程序外,現(xiàn)在使用用戶線程的程序越來越少了,Java、Ruby等語言都曾經(jīng)使用過用戶線程,最終又都放棄使用它。

  • 3 使用用戶線程加輕量級進(jìn)程混合實現(xiàn)

線程除了依賴內(nèi)核線程實現(xiàn)和完全由用戶程序自己實現(xiàn)之外,還有一種將內(nèi)核線程與用戶線程一起使用的實現(xiàn)方式。 在這種混合實現(xiàn)下,既存在用戶線程,也存在輕量級進(jìn)程。 用戶線程還是完全建立在用戶空間中,因此用戶線程的創(chuàng)建、 切換、 析構(gòu)等操作依然廉價,并且可以支持大規(guī)模的用戶線程并發(fā)。 而操作系統(tǒng)提供支持的輕量級進(jìn)程則作為用戶線程和內(nèi)核線程之間的橋梁,這樣可以使用內(nèi)核提供的線程調(diào)度功能及處理器映射,并且用戶線程的系統(tǒng)調(diào)用要通過輕量級線程來完成,大大降低了整個進(jìn)程被完全阻塞的風(fēng)險。 在這種混合模式中,用戶線程與輕量級進(jìn)程的數(shù)量比是不定的,即為N:M的關(guān)系,如下圖所示,這種

圖5.png

參考資料

<<深入理解Java虛擬機(jī)>>

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容