JVM-2:Java內(nèi)存模型

一、JMM的必要性

眾所周知,數(shù)據(jù)競(jìng)爭(zhēng)(Data Racing)在并發(fā)編程中是個(gè)重要問(wèn)題。操作系統(tǒng)的很大一部分任務(wù)就是在協(xié)調(diào)資源的分配,尤其是內(nèi)存資源的分配。例如,線程A和線程B同時(shí)獲取一個(gè)共享內(nèi)存中的int變量,誰(shuí)應(yīng)該優(yōu)先獲取這個(gè)變量呢?從數(shù)據(jù)競(jìng)爭(zhēng)衍生出的一個(gè)新問(wèn)題則是線程間的通信問(wèn)題,即內(nèi)存可見(jiàn)性問(wèn)題。線程間需要通信則是由線程共享處理器產(chǎn)生的,通常線程在Ready、Running、Blocked三個(gè)狀態(tài)中不斷切換,直到線程結(jié)束。

States of a thread
因此,每個(gè)線程都無(wú)法保證使用內(nèi)存資源時(shí)的“原子操作”,也就是會(huì)產(chǎn)生內(nèi)存可見(jiàn)性問(wèn)題。線程在更新內(nèi)存時(shí)的狀態(tài):
線程更新內(nèi)存

不僅線程狀態(tài)切換可以導(dǎo)致內(nèi)存可見(jiàn)性問(wèn)題。為了提升處理器性能,編譯器在生成可執(zhí)行指令以及處理器在執(zhí)行指令時(shí)會(huì)對(duì)指令進(jìn)行重排序。關(guān)于重排序,請(qǐng)參閱:

重排序改變了程序編寫(xiě)時(shí)應(yīng)有的順序,因此產(chǎn)生了內(nèi)存可見(jiàn)性問(wèn)題。為了解決由線程切換和指令重排序產(chǎn)生的內(nèi)存可見(jiàn)性問(wèn)題,Java語(yǔ)言層面的內(nèi)存模型提供了相應(yīng)的解決方法,即Java內(nèi)存模型(JMM)。

二、JMM的內(nèi)存可見(jiàn)性解決方法

1. 重排序規(guī)則限制

JMM在編譯期間遵循了相關(guān)的指令重排序限制,以保證內(nèi)存對(duì)相關(guān)線程可見(jiàn)。

  • 遵守?cái)?shù)據(jù)依賴(lài)性: 在重排序過(guò)程中,編譯器和處理器不會(huì)改變存在數(shù)據(jù)依賴(lài)關(guān)系的兩個(gè)操作的執(zhí)行順序。
    數(shù)據(jù)依賴(lài)性:如果兩個(gè)操作訪問(wèn)同一個(gè)變量,且這兩個(gè)操作中有一個(gè)為寫(xiě)操作,此時(shí)這兩個(gè)操作之間就存在數(shù)據(jù)依賴(lài)性。
  • 遵從as-if-serial原則: 不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變。編譯器,runtime和處理器都必須遵守as-if-serial語(yǔ)義。

也就是說(shuō),沒(méi)有數(shù)據(jù)依賴(lài)關(guān)系的操作有可能會(huì)被編譯器或處理器重排序。下面是一個(gè)計(jì)算長(zhǎng)方形周長(zhǎng)的例子:

int width = 10; // a
int length = 15; // b 
int perimeter = (width + length) * 2; // c

a, b, c的依賴(lài)關(guān)系有:

  • a ---> c
  • b ---> c

也就是c依賴(lài)于a操作和b操作,但是a操作和b操作不存在依賴(lài)關(guān)系。那么程序執(zhí)行順序有如下可能:

  • a ---> b ---> c 按順序執(zhí)行,結(jié)果為50
  • b ---> a ---> c 重排序執(zhí)行,結(jié)果為50

從上述結(jié)果可以得知:as-if-serial語(yǔ)義保證了程序的單線程執(zhí)行結(jié)果不會(huì)被改變。而程序員在編寫(xiě)時(shí)并不知道編譯后的操作順序和處理器執(zhí)行操縱的順序,但也不用擔(dān)心重排序會(huì)對(duì)我們想要的結(jié)果產(chǎn)生干擾。

2. 關(guān)鍵字保護(hù)

在JSR133中,JMM分別增強(qiáng)了final, volatile, synchronized這三個(gè)關(guān)鍵字的內(nèi)存語(yǔ)義。在編譯期和處理器運(yùn)行指令時(shí),有這三個(gè)關(guān)鍵字的指令將受到重排序保護(hù),相關(guān)的指令不會(huì)被重排序。一起來(lái)看看JMM是如何實(shí)現(xiàn)這些保護(hù)的。

三、 關(guān)鍵字保護(hù)

1. Volatile

1.1 Volatile語(yǔ)義

當(dāng)一個(gè)共享變量聲明為volatile后,該變量的讀/寫(xiě)將會(huì)很特別。被volatile保護(hù)的變量相當(dāng)于改變量的讀/寫(xiě)操作被鎖保護(hù)起來(lái)了。來(lái)看下面兩段代碼(改自程曉明文章):

class VolatileProtection {
    volatile long varOne = 0L;  // 使用volatile聲明64位的long型變量
    public voiid set(long l) {
        varOne = l;             // volatile變量的單個(gè)寫(xiě)操作
    }
    public void increase() {
        varOne++;               // volatile變量的復(fù)合(多個(gè))讀/寫(xiě)操作
    }
    public long get(){
        return varOne;          // volatile變量的單個(gè)讀操作
    }
}

假設(shè)有多個(gè)線程分別調(diào)用VolatileProtection中的setincreaseget方法,那么上述程序?qū)⒂泻鸵韵鲁绦蛳嗤男Ч?/p>

class SynchronizedProtection {
    long varOne = 0L;          // 64位的long型普通變量
    public synchronized void set(long l) {    // 用鎖同步普通變量的單個(gè)寫(xiě)操作
        varOne = l;             
    }
    public void increase() {   // 普通方法調(diào)用
        long temp = get();     // 調(diào)用已同步的讀方法
        temp += 1L;            // 普通寫(xiě)操作
        set(temp);             // 調(diào)用已同步的寫(xiě)方法
    } 
    public synchronized long get() {         // 用鎖同步普通變量的單個(gè)讀操作
        return varOne;
    }
}

鎖的語(yǔ)義決定了get()方法和set()方法的操作具有原子性。同樣,受volatile保護(hù)的變量在讀/寫(xiě)操作上也具有原子性。volatile的特性可以總結(jié)為:

  • 可見(jiàn)性:一個(gè)volatile變量的讀,總是能看到任意線程對(duì)這個(gè)volatile變量最后的寫(xiě)入
  • 原子性:volatile變量的單個(gè)讀/寫(xiě)句有原子性,但類(lèi)似于volatile++這種復(fù)合操作不具原子性。

1.2 Volatile的內(nèi)存語(yǔ)義

我們已經(jīng)知道volatile變量的寫(xiě)/讀具有原子性,那么volatile變量是如何在內(nèi)存中實(shí)現(xiàn)這些語(yǔ)義的呢?來(lái)看看volatile寫(xiě)和讀的內(nèi)存語(yǔ)義。

  • Volatile寫(xiě):當(dāng)我們往共享內(nèi)存中寫(xiě)入一個(gè)volatile變量時(shí),JMM會(huì)把對(duì)應(yīng)線程中的本地內(nèi)存中的貢獻(xiàn)變量值寫(xiě)入主內(nèi)存(即共享內(nèi)存)。
  • Volatile讀:當(dāng)我們讀取一個(gè)volatile變量時(shí),JMM會(huì)把對(duì)應(yīng)線程的本地內(nèi)存中現(xiàn)有的變量重置為無(wú)效,緊接著會(huì)從主內(nèi)存中讀取共享變量值。

1.3 Volatile內(nèi)存語(yǔ)義的實(shí)現(xiàn)

前面說(shuō)到JMM會(huì)在讀volatile變量時(shí)重置本地內(nèi)存,并在寫(xiě)volatile變量時(shí)將線程本地內(nèi)存中的值刷入共享內(nèi)存。在線程不斷切換狀態(tài)讓出處理器的情況下,JMM如何保證這些操作的原子性呢? 這就涉及到JMM實(shí)現(xiàn)volatile讀/寫(xiě)的內(nèi)存語(yǔ)義的方法。

JMM對(duì)編譯器制定了有關(guān)volatile重排序的規(guī)則表:

是否能重排序 第二個(gè)操作
第一個(gè)操作 普通讀/寫(xiě) volatile讀 volatile寫(xiě)
普通讀/寫(xiě) NO
volatile讀 NO NO NO
volatile寫(xiě) NO NO

由上表我們可以得知,JMM通過(guò)禁止與volatile讀/寫(xiě)相關(guān)的重排序來(lái)保證volatile變量操作的原子性。為了實(shí)現(xiàn)相關(guān)指令的重排序保護(hù),編譯器會(huì)在volatile讀/寫(xiě)操作的指令前后添加相關(guān)屏障(Barrier),因此處理器無(wú)法越過(guò)屏障進(jìn)行重排序。

2. Final

2.1 Final的語(yǔ)義

對(duì)于final域,編譯器和處理器遵循以下兩個(gè)重排序規(guī)則:

  • 在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè)final域的寫(xiě)入,與隨后把這個(gè)被構(gòu)造對(duì)象的引用賦值給一個(gè)引用變量,這兩個(gè)操作之間不能重排序。
  • 初次讀一個(gè)包含final域的對(duì)象的引用,與隨后初次讀這個(gè)final域,這兩個(gè)操作不能重排序。
public class FinalExample {
    int i;                   // 普通變量
    final int j;             // final 變量
    static FinalExample obj;
    
    public void FinalExample() {    // 構(gòu)造函數(shù)
        i = 1;                      // 寫(xiě)普通域  (可能被重排序到構(gòu)造函數(shù)之外)
        j = 2;                      // 寫(xiě)final域 (不會(huì)被重排序到構(gòu)造函數(shù)之外)
    }
    
    public static void writer() {   // 寫(xiě)線程A執(zhí)行
        obj = new FinalExample(); 
    }
    
    public static void reader() {   // 讀線程B執(zhí)行
        FinalExample object = obj;  // 初次讀對(duì)象引用  a
        int a = object.i;           // 初次讀普通域    b
        int b = object.j;           // 初次讀final域   c (a與c被禁止重排序)
    }
}

2.2 Final域的重排序規(guī)則

  • 寫(xiě)Final域:

    • JMM禁止編譯器把final域的寫(xiě)重排序到構(gòu)造函數(shù)之外。編譯器通過(guò)在final域的寫(xiě)操作之后,構(gòu)造函數(shù)return之前,插入一個(gè)StoreStore屏障來(lái)達(dá)到緊致重排序的目的。
  • 讀Final域:

    • 在一個(gè)線程中,JMM禁止處理器重排序以下兩個(gè)操作:

      • 初次讀對(duì)象引用
      • 初次讀該對(duì)象包含的final

      編譯器通過(guò)在讀final域操作的前面插入一個(gè)LoadLoad屏障來(lái)實(shí)現(xiàn)禁止重排序。

個(gè)人認(rèn)為寫(xiě)final域的重排序規(guī)則比較晦澀,因?yàn)槊總€(gè)構(gòu)造函數(shù)中的操作都應(yīng)該禁止被重排序到構(gòu)造函數(shù)結(jié)束之外。假設(shè)有操作被重排序到構(gòu)造函數(shù)結(jié)束后,那么這個(gè)對(duì)象算是初始化完成了還是未完成呢?按理說(shuō)構(gòu)造函數(shù)完成了,對(duì)象初始化完成;可是構(gòu)造函數(shù)里邊的操作并沒(méi)有結(jié)束,相關(guān)域還沒(méi)被初始化,對(duì)象不能算完成構(gòu)建。所以對(duì)我而言,寫(xiě)Final域不需要重排序,換而言之,構(gòu)造函數(shù)里的所有操作都必須被禁止重排序到構(gòu)造函數(shù)結(jié)束之后。

讀Final域的重排序規(guī)則比較容易理解:因?yàn)槌醮巫x對(duì)象引用的操作a相當(dāng)于初始化FinalExample類(lèi)型的引用變量object,而初次讀object.j操作c必須要基于object已經(jīng)被初始化了的基礎(chǔ)之上,顯然不能重排序。

2.3 final引用不能從構(gòu)造函數(shù)逸出

  • 寫(xiě)Final域的另一個(gè)重排序規(guī)則:
    • 在引用變量為任意線程可見(jiàn)之前,該引用變量指向的對(duì)象的final域已經(jīng)在構(gòu)造函數(shù)中被正確初始化了。也就是不能讓這個(gè)被構(gòu)造對(duì)象的引用為其他線程可見(jiàn)。

四、鎖

除了相關(guān)重排序規(guī)則和關(guān)鍵字保護(hù)以外,Java鎖也提供了內(nèi)存可見(jiàn)性問(wèn)題的解決方法。

鎖可以保證臨界區(qū)內(nèi)的操作具有原子性,從而解決內(nèi)存可見(jiàn)性問(wèn)題。Java的用volatile來(lái)實(shí)對(duì)state的保護(hù),即保證每次獲取鎖和釋放鎖都具有原子操作。

五、總結(jié)

JMM主要通過(guò)禁止相關(guān)指令的重排序來(lái)解決內(nèi)存可見(jiàn)性問(wèn)題。不管是關(guān)鍵字volatile,final,還是鎖,都使用禁止重排序的方法來(lái)實(shí)現(xiàn)相關(guān)功能。

參考

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

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

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