Java內(nèi)存模型與線程

一、QA

計算機(jī)硬件中高速緩存的作用是什么?

  • 內(nèi)存讀寫速度與處理器運(yùn)算速度相比有幾個數(shù)量級的差距,所以現(xiàn)代計算機(jī)都會在處理器與內(nèi)存之間加入高速緩存來作為內(nèi)存與處理器之間的緩沖:將運(yùn)算需要使用到的數(shù)據(jù)復(fù)制到高速緩存中,讓運(yùn)算能快速進(jìn)行,當(dāng)運(yùn)算結(jié)束后再從高速緩存同步到內(nèi)存中,這樣處理器就無需等待緩慢的內(nèi)存讀寫了。

在處理器與內(nèi)存之間加入高速緩存會出現(xiàn)什么問題,這個問題又是怎么解決的?

  • 會出現(xiàn)緩存一致性問題,因?yàn)槊總€處理器都有自己的高速緩存,而它們又共享同一主內(nèi)存,當(dāng)多個處理器的運(yùn)算任務(wù)都涉及到同一塊主內(nèi)存區(qū)域時,將可能導(dǎo)致各自的緩存數(shù)據(jù)不一致,那么再次同步回主內(nèi)存時,不知道哪個高速緩存中的數(shù)據(jù)是正確的。

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

    image

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

  • 內(nèi)存模型可以理解為在特定操作協(xié)議下,對特定的內(nèi)存或高速緩存進(jìn)行讀寫訪問的過程抽象。

為什么要定義Java內(nèi)存模型?它的定義的什么?

  • 為了屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)讓java程序員在各種平臺下都能達(dá)到一致的內(nèi)存訪問效果。
  • 它主要定義了程序中各種變量的訪問規(guī)則,這里的變量指的是實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對象的元素。為了獲得較好的執(zhí)行效能,java內(nèi)存模型并沒有限制執(zhí)行引擎使用處理器的特定寄存器或緩存來和主內(nèi)存進(jìn)行交互,也沒有限制即時編譯器進(jìn)行調(diào)整代碼執(zhí)行順序這類優(yōu)化措施。

不同硬件與操作系統(tǒng)的內(nèi)存訪問是有差異的,但有了JVM這一層適配,做到了java程序在各種平臺都能達(dá)到一致的內(nèi)存訪問效果。JVM之所以能夠做好內(nèi)存訪問的適配,是因?yàn)镴ava內(nèi)存模型(JMM)定義了內(nèi)存訪問規(guī)則。

Java內(nèi)存模型中主內(nèi)存與工作內(nèi)存的關(guān)系是什么?

  • 所有變量都存儲在主內(nèi)存。

  • 每條線程還有自己的工作內(nèi)存。

  • 線程的工作內(nèi)存中保存了被該線程使用到的變量的主內(nèi)存副本拷貝。(不會有虛擬機(jī)的實(shí)現(xiàn)把整個對象拷貝一份)

  • 線程對變量的所有操作(讀取、賦值等)都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的變量。(即使是volatile變量也存在內(nèi)存的拷貝,它只是看起來像是直接讀寫主內(nèi)存)

  • 不同線程之間無法直接訪問對方工作內(nèi)存中的變量。

  • 線程間變量值的傳遞均需要通過主內(nèi)存來完成。

    image

主內(nèi)存與工作內(nèi)存的具體交互協(xié)議是什么?

  • java內(nèi)存模型定義了以下8種操作來完成一個變量從主內(nèi)存拷貝到工作內(nèi)存和從工作內(nèi)存同步回主內(nèi)存之間的實(shí)現(xiàn)細(xì)節(jié)。虛擬機(jī)實(shí)現(xiàn)時必須保證下面提及的每一種操作都是原子的、不可再分的(對于double和long類型的變量來說,load、store、read和write操作在某些平臺上允許有例外)。

    • lock(鎖定):作用于主內(nèi)存的變量,它把一個變量標(biāo)識為一條線程獨(dú)占的狀態(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)虛擬機(jī)遇到一個需要使用到變量的值的字節(jié)碼指令時將會執(zhí)行這個操作。
    • assign(賦值):作用于工作內(nèi)存的變量,它把從執(zhí)行引擎接收到的值賦給工作內(nèi)存中的變量,每當(dāng)虛擬機(jī)遇到一個給變量賦值的字節(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操作之一單獨(dú)出現(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)的變量,換句話說,就是對一個變量實(shí)施use、store操作之前,必須先執(zhí)行過了assign和load操作。
    • 一個變量在同一時刻只允許一條線程對其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有相同次數(shù)的unlock操作,變量才會被解鎖。
    • 如果一個變量執(zhí)行l(wèi)ock操作,那將會清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個變量前,需要重新load或assign操作初始化變量的值。
    • 如果一個變量實(shí)現(xiàn)沒有被lock操作鎖定,那就不允許對它進(jìn)行unlock操作,也不允許去unlock一個被其他線程鎖定的變量。
    • 對一個變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)。
  • 對volatile型變量的特殊規(guī)則

    假定T表示一個線程,V和W分別表示兩個volatile型變量,那么在進(jìn)行read、load、use、assign、store和write操作時需要滿足如下規(guī)則:

    • 只有當(dāng)線程T對變量V執(zhí)行的前一個動作是load的時候,線程T才能對變量V執(zhí)行use動作;并且,只有當(dāng)線程T對變量V執(zhí)行的后一個動作是use的時候,線程T才能對變量V執(zhí)行l(wèi)oad操作。線程T對變量V的use動作可以認(rèn)為是和線程T對變量V的load、read動作相關(guān)聯(lián),必須連續(xù)出現(xiàn)(這條規(guī)則要求在工作內(nèi)存中,每次使用V前都必須先從主內(nèi)存刷新最新的值,用于保證能看見其他線程對變量V所作的修改后的值)。
    • 只有當(dāng)線程T對變量V執(zhí)行的前一個動作是assign的時候,線程T才能對變量V執(zhí)行store動作;并且,之后當(dāng)線程T對變量V執(zhí)行的后一個動作是store的時候,線程T才能對變量V執(zhí)行assign動作。線程T對變量V的assign動作可以認(rèn)為是和線程T對變量V的stored、write動作相關(guān)聯(lián),必須連續(xù)一起出現(xiàn)(這條規(guī)則要求在工作內(nèi)存中,每次修改V后都必須立刻同步回主內(nèi)存中,用于保證其他線程可以看到自己對變量V所做的修改);
    • 假定動作A是線程T對變量V實(shí)施use或assign動作,假定動作F是和動作A相關(guān)聯(lián)的load或store動作,假定動作P是和動作F相應(yīng)的對變量V的read或write動作;類似的,假定動作B是線程T對變量W實(shí)施的use或assign動作,假定動作G是和動作B相關(guān)聯(lián)的load或store動作,假定動作Q是和動作G相應(yīng)的對變量W的read或write動作。如果A先于B,那么P先于Q(這條規(guī)則要求volatile修飾的變量不會被指定重排序優(yōu)化,保證代碼的執(zhí)行順序與程序的順序相同)。
  • 8種內(nèi)存訪問操作、這些操作的規(guī)則限定和volatile的一些特殊規(guī)定,就已經(jīng)完全確定了java程序中哪些內(nèi)存訪問操作在并發(fā)下是安全的。由于這種定義相當(dāng)嚴(yán)謹(jǐn)和煩瑣,所以還有一個和這種定義等效的判斷原則——先行發(fā)生原則(happen-before),用來確定一個訪問在并發(fā)環(huán)境下是否安全。

volatile的作用是什么?

什么是happens-before原則?

它是判斷數(shù)據(jù)是否存在競爭、線程是否安全的主要依據(jù),依靠這個原則,我們可以通過幾條規(guī)則一攬子地解決并發(fā)環(huán)境下兩個操作之間是否可能存在沖突的所有問題。

先行發(fā)生是Java內(nèi)存模型中定義的兩項(xiàng)操作之間的偏序關(guān)系,如果說操作A先行發(fā)生與操作B,其實(shí)就是說在發(fā)生操作B之前,操作A產(chǎn)生的影響能被B觀察到,“影響”包括修改了內(nèi)存中共享變量的值、發(fā)送了消息、調(diào)用了方法等。

下面是Java內(nèi)存模型下一些“天然的”先行發(fā)生關(guān)系,這些先行發(fā)生關(guān)系無須任何同步器協(xié)助就已經(jīng)存在,可以在編碼中直接使用。如果兩個操作之間的關(guān)系不在此列,并且無法從下列規(guī)則推導(dǎo)出來的話,他們就沒有順序性保障,虛擬機(jī)可以對他們隨意地進(jìn)行重排序。

  • 程序次序規(guī)則(Program Order Rule):在一個線程內(nèi),按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。準(zhǔn)確地說,應(yīng)該是控制流順序而不是程序代碼順序,因?yàn)橐紤]分支、循環(huán)等結(jié)構(gòu)。
  • 管程鎖定規(guī)則(Monitor Lock Rule):一個unlock操作先行發(fā)生于后面對同一個鎖的lock操作。這里必須強(qiáng)調(diào)的是同一個鎖,而“后面”是指時間上的先后順序。
  • volatile變量規(guī)則(Volatile Variable Rule):對一個volatile變量的寫操作先行發(fā)生于后面對這個變量的讀操作,這里的“后面”同樣是指時間上的先后順序。
  • 線程啟動規(guī)則(Thread Start Rule):Thread對象的start方法先行發(fā)生于此線程的每一個動作。
  • 線程終止規(guī)則(Thread Termination Rule):線程中的所有操作都先行發(fā)生于對此線程的終止檢測,我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值等手段檢測到線程已經(jīng)終止執(zhí)行。
  • 線程中斷規(guī)則(Thread Interruption Rule):對線程interrupt方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過Thread.interrupted()方法檢測到是否有中斷發(fā)生。
  • 對象終結(jié)規(guī)則(Finalizer Rule):一個對象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的finalize方法的開始。
  • 傳遞性(Transitivity):如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結(jié)論。

舉一個先行發(fā)生原則的例子

private int value = 0;
public void setValue(int value) {
    this.value = value;
}
public int getValue() {
    return value;
}

假設(shè)存在線程A和線程B,線程A先(時間上)調(diào)用了setValue(1),然后線程B調(diào)用了同一個對象的getValue(),那么線程B收到的返回值是什么?

  • 由于兩個方法分別有線程A和線程B調(diào)用,不在一個線程中,所以程序次序規(guī)則在這里不適用;
  • 由于沒有同步塊,自然就不會發(fā)生lock和unlock操作,所以管程鎖定規(guī)則不適用;
  • 由于value變量沒有被volatile關(guān)鍵字修飾,所以volatile變量規(guī)則不適用;
  • 后面的線程啟動、終止、中斷規(guī)則和對象終結(jié)規(guī)則也和這里沒有關(guān)系;
  • 因?yàn)闆]有一個適合的先行發(fā)生規(guī)則,所以最后一條傳遞性也無從談起。

因此我們可以判定盡管線程A在操作時間上先于線程B,但是無法確定線程B中g(shù)etValue的返回結(jié)果,換句話說,這里面的操作不是線程安全的。

那么怎么修復(fù)這個問題呢?我們至少有兩種比較簡單的方案可以選擇:

  • 要么把getter/setter方法都定義為synchronized方法,這樣就可以套用管程鎖定規(guī)則
  • 要么把value定義為volatile變量,由于setter方法對value的修改不依賴value的原值,滿足volatile關(guān)鍵字使用場景,這樣就可以套用volatile變量規(guī)則來實(shí)現(xiàn)先行發(fā)生關(guān)系。

結(jié)論:一個操作“時間上的先發(fā)生”不代表這個操作會是“先行發(fā)生”,那如果一個操作“先行發(fā)生”是否就能推導(dǎo)出這個操作必須是“時間上的先發(fā)生”呢?很遺憾,這個推論也不成立,一個典型的例子就是“指令重排序”。

// 以下操作在同一個線程中執(zhí)行
int i = 1;
int j = 2;

上面的代碼中,兩條賦值語句在同一個線程中,根據(jù)程序次序規(guī)則,int i = 1的操作先行發(fā)生于int j = 2,但是int j = 2的代碼完全可能先被處理器執(zhí)行,這并不影響先行發(fā)生原則的正確性,因?yàn)槲覀冊谶@條線程之中沒有辦法感知這點(diǎn)。

上面兩個例子綜合起來證明了一個結(jié)論:時間上先后順序與先行發(fā)生原則之間基本沒有太大的關(guān)系,所以我們衡量并發(fā)安全問題的時候不要受到時間順序的干擾,一切必須以先行發(fā)生原則為準(zhǔn)。

Java內(nèi)存模型是圍繞著在并發(fā)過程中如何處理原子性、可見性和有序性這3個特征來建立的,那么哪些操作實(shí)現(xiàn)了這3個特征?

  • 原子性(Atomicity):

    • 由Java內(nèi)存模型來直接保證的原子性變量操作包括read、load、use、store和write,我們大致可以認(rèn)為基本數(shù)據(jù)類型的訪問讀寫是具備原子性的(除了long和double的非原子性協(xié)定)。

    • 如果應(yīng)用場景需要一個更大范圍的原子性保證,Java內(nèi)存模型還提供了lock和unlock操作來滿足這種需求,盡管虛擬機(jī)未把lock和unlock操作直接開放給用戶使用,但是卻提供了更高層次的字節(jié)碼指令monitorenter和monitorexit來隱式地使用這兩個操作,這兩個字節(jié)碼指令反映到Java代碼中就是同步塊——synchronized關(guān)鍵字,因此在synchronized塊之間的操作也具備原子性。

  • 可見性(Visibility):

    • 可見性是指當(dāng)一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。上文在講解volatile變量的時候我們已詳細(xì)討論過這一點(diǎn)。Java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方式來實(shí)現(xiàn)可見性,無論是普通變量還是volatile變量都是如此,普通變量與volatile變量的區(qū)別是,volatile的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新。因此可以說volatile保證了多線程操作時變量的可見性,而普通變量則不能保證這一點(diǎn)。

    • 除了volatile之外,Java還有兩個關(guān)鍵字能實(shí)現(xiàn)可見性,即synchronized和final。同步塊的可見性是由“對一個變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)”這條規(guī)則獲得的,而final關(guān)鍵字的可見性是指:被final修飾的字段在構(gòu)造器中一旦初始化完成,并且構(gòu)造器沒有把“this”的引用傳遞出去(this引用逃逸是一件很危險的事情,其他線程有可能通過這個引用訪問到“初始化了一半”的對象),那在其他線程中就能看見final字段的值。

  • 有序性(Ordering):

    • Java內(nèi)存模型的有序性在前面講解volatile時也詳細(xì)地討論過了,Java程序中天然的有序性可以總結(jié)為一句話:如果在本線程內(nèi)觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有操作都是無序的。前半句是指“線程內(nèi)表現(xiàn)為串行的語義”(Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”現(xiàn)象和“工作內(nèi)存與主內(nèi)存同步延遲”現(xiàn)象。

    • Java語言提供了volatile和synchronized兩個關(guān)鍵字來保證線程之間操作的有序性,volatile關(guān)鍵字本身就包含了禁止指令重排序的語義,而synchronized則是由“一個變量在同一時刻只允許一條線程對其進(jìn)行l(wèi)ock操作”這條規(guī)則獲得的,這條規(guī)則決定了持有同一個鎖的兩個同步塊只能串行地進(jìn)入。

二、其他

  • 絕大多數(shù)的運(yùn)算任務(wù)都不可能只靠處理器“計算”就能完成,處理器至少要與內(nèi)存交互,如讀取運(yùn)算數(shù)據(jù)、存儲運(yùn)算結(jié)果等,這個IO操作是很難消除的(無法僅靠寄存器來完成所有運(yùn)算任務(wù))。

  • 處理器高速緩存的緩存一致性協(xié)議有 MSI、MESI、MOSI、Synapse、Firefly 和 Dragon Protocol 等。

  • 除了增加高速緩存之外,為了使得處理器內(nèi)部的運(yùn)算單元能盡量被充分利用,處理器可能會對輸入代碼進(jìn)行亂序執(zhí)行(out-of-order Execution)優(yōu)化,處理器會在計算之后將亂序執(zhí)行的結(jié)果重組,保證該結(jié)果與順序執(zhí)行的結(jié)果是一致的,因此如果存在一個計算任務(wù)依賴另一個計算任務(wù)的中間結(jié)果,那么其順序性并不能靠代碼的先后順序來保證。

  • 主內(nèi)存就直接對應(yīng)于物理硬件的內(nèi)存,而為了獲取更好的運(yùn)行速度,虛擬機(jī)(甚至是硬件系統(tǒng)本身的優(yōu)化措施)可能會讓工作內(nèi)存優(yōu)先存儲于寄存器和高速緩存中,因?yàn)槌绦蜻\(yùn)行時主要訪問讀寫的是工作內(nèi)存。

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

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

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