溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

怎么深入理解Java內(nèi)存模型JMM

發(fā)布時(shí)間:2021-11-20 15:16:35 來(lái)源:億速云 閱讀:149 作者:柒染 欄目:軟件技術(shù)

這期內(nèi)容當(dāng)中小編將會(huì)給大家?guī)?lái)有關(guān)怎么深入理解Java內(nèi)存模型JMM,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

Java 內(nèi)存模型

Java 內(nèi)存模型(JMM)是一種抽象的概念,并不真實(shí)存在,它描述了一組規(guī)則或規(guī)范,通過(guò)這組規(guī)范定義了程序中各個(gè)變量(包括實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素)的訪問(wèn)方式。試圖屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異,以實(shí)現(xiàn)讓 Java 程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問(wèn)效果。

注意JMM與JVM內(nèi)存區(qū)域劃分的區(qū)別:

  • JMM描述的是一組規(guī)則,圍繞原子性、有序性和可見(jiàn)性展開;

  • 相似點(diǎn):存在共享區(qū)域和私有區(qū)域

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

處理器上的寄存器的讀寫的速度比內(nèi)存快幾個(gè)數(shù)量級(jí),為了解決這種速度矛盾,在它們之間加入了高速緩存。

加入高速緩存帶來(lái)了一個(gè)新的問(wèn)題:緩存一致性。如果多個(gè)緩存共享同一塊主內(nèi)存區(qū)域,那么多個(gè)緩存的數(shù)據(jù)可能會(huì)不一致,需要一些協(xié)議來(lái)解決這個(gè)問(wèn)題。

怎么深入理解Java內(nèi)存模型JMM

所有的變量都 存儲(chǔ)在主內(nèi)存中,每個(gè)線程還有自己的工作內(nèi)存 ,工作內(nèi)存存儲(chǔ)在高速緩存或者寄存器中,保存了該線程使用的變量的主內(nèi)存副本拷貝。

線程只能直接操作工作內(nèi)存中的變量,不同線程之間的變量值傳遞需要通過(guò)主內(nèi)存來(lái)完成。

怎么深入理解Java內(nèi)存模型JMM

數(shù)據(jù)存儲(chǔ)類型以及操作方式

  • 方法中的基本類型本地變量將直接存儲(chǔ)在工作內(nèi)存的棧幀結(jié)構(gòu)中;

  • 引用類型的本地變量:引用存儲(chǔ)在工作內(nèi)存,實(shí)際存儲(chǔ)在主內(nèi)存;

  • 成員變量、靜態(tài)變量、類信息均會(huì)被存儲(chǔ)在主內(nèi)存中;

  • 主內(nèi)存共享的方式是線程各拷貝一份數(shù)據(jù)到工作內(nèi)存中,操作完成后就刷新到主內(nèi)存中。

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

Java 內(nèi)存模型定義了 8 個(gè)操作來(lái)完成主內(nèi)存和工作內(nèi)存的交互操作。

怎么深入理解Java內(nèi)存模型JMM

  • read:把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)焦ぷ鲀?nèi)存中

  • load:在 read 之后執(zhí)行,把 read 得到的值放入工作內(nèi)存的變量副本中

  • use:把工作內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎

  • assign:把一個(gè)從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量

  • store:把工作內(nèi)存的一個(gè)變量的值傳送到主內(nèi)存中

  • write:在 store 之后執(zhí)行,把 store 得到的值放入主內(nèi)存的變量中

  • lock:作用于主內(nèi)存的變量

  • unlock

指令重排序的條件

  • 在單線程環(huán)境下不能改變程序的運(yùn)行結(jié)果;

  • 存在數(shù)據(jù)依賴關(guān)系的不允許重排序;

  • 無(wú)法通過(guò)Happens-before原則推到出來(lái)的,才能進(jìn)行指令的重排序。

內(nèi)存模型三大特性

1. 原子性

Java 內(nèi)存模型保證了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如對(duì)一個(gè) int 類型的變量執(zhí)行 assign 賦值操作,這個(gè)操作就是原子性的。但是 Java 內(nèi)存模型允許虛擬機(jī)將沒(méi)有被 volatile 修飾的 64 位數(shù)據(jù)(long,double)的讀寫操作劃分為兩次 32 位的操作來(lái)進(jìn)行,即 load、store、read 和 write 操作可以不具備原子性。

有一個(gè)錯(cuò)誤認(rèn)識(shí)就是,int 等原子性的類型在多線程環(huán)境中不會(huì)出現(xiàn)線程安全問(wèn)題。前面的線程不安全示例代碼中,cnt 屬于 int 類型變量,1000 個(gè)線程對(duì)它進(jìn)行自增操作之后,得到的值為 997 而不是 1000。

為了方便討論,將內(nèi)存間的交互操作簡(jiǎn)化為 3 個(gè):load、assign、store。

下圖演示了兩個(gè)線程同時(shí)對(duì) cnt 進(jìn)行操作,load、assign、store 這一系列操作整體上看不具備原子性,那么在 T1 修改 cnt 并且還沒(méi)有將修改后的值寫入主內(nèi)存,T2 依然可以讀入舊值??梢钥闯?,這兩個(gè)線程雖然執(zhí)行了兩次自增運(yùn)算,但是主內(nèi)存中 cnt 的值最后為 1 而不是 2。因此對(duì) int 類型讀寫操作滿足原子性只是說(shuō)明 load、assign、store 這些單個(gè)操作具備原子性。

怎么深入理解Java內(nèi)存模型JMM

AtomicInteger 能保證多個(gè)線程修改的原子性。

怎么深入理解Java內(nèi)存模型JMM

使用 AtomicInteger 重寫之前線程不安全的代碼之后得到以下線程安全實(shí)現(xiàn):

public class AtomicExample {    private AtomicInteger cnt = new AtomicInteger();    public void add() {
        cnt.incrementAndGet();
    }    public int get() {        return cnt.get();
    }
}復(fù)制代碼
public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    AtomicExample example = new AtomicExample(); // 只修改這條語(yǔ)句
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
}復(fù)制代碼
1000復(fù)制代碼

除了使用原子類之外,也可以使用 synchronized 互斥鎖來(lái)保證操作的原子性。它對(duì)應(yīng)的內(nèi)存間交互操作為:lock 和 unlock,在虛擬機(jī)實(shí)現(xiàn)上對(duì)應(yīng)的字節(jié)碼指令為 monitorenter 和 monitorexit。

public class AtomicSynchronizedExample {    private int cnt = 0;    public synchronized void add() {
        cnt++;
    }    public synchronized int get() {        return cnt;
    }
}復(fù)制代碼
public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    AtomicSynchronizedExample example = new AtomicSynchronizedExample();
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }    countDownLatch.await();    executorService.shutdown();    System.out.println(example.get());
}復(fù)制代碼
1000復(fù)制代碼

2. 可見(jiàn)性

可見(jiàn)性指當(dāng)一個(gè)線程修改了共享變量的值,其它線程能夠立即得知這個(gè)修改。Java 內(nèi)存模型是通過(guò)在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值來(lái)實(shí)現(xiàn)可見(jiàn)性的。JMM 內(nèi)部的實(shí)現(xiàn)通常是依賴于所謂的 內(nèi)存屏障 ,通過(guò) 禁止某些重排序 的方式,提供內(nèi)存 可見(jiàn)性保證 ,也就是實(shí)現(xiàn)了 各種 happen-before 規(guī)則 。與此同時(shí),更多復(fù)雜度在于,需要盡量確保各種編譯器、各種體系結(jié)構(gòu)的處理器,都能夠提供一致的行為。

主要有有三種實(shí)現(xiàn)可見(jiàn)性的方式:

  • volatile,會(huì) 強(qiáng)制 將該變量自己和當(dāng)時(shí)其他變量的狀態(tài)都 刷出緩存 。

  • synchronized,對(duì)一個(gè)變量執(zhí)行 unlock 操作之前,必須把變量值同步回主內(nèi)存。

  • final,被 final 關(guān)鍵字修飾的字段在構(gòu)造器中一旦初始化完成,并且沒(méi)有發(fā)生 this 逃逸(其它線程通過(guò) this 引用訪問(wèn)到初始化了一半的對(duì)象),那么其它線程就能看見(jiàn) final 字段的值。

對(duì)前面的線程不安全示例中的 cnt 變量使用 volatile 修飾,不能解決線程不安全問(wèn)題,因?yàn)?volatile 并不能保證操作的原子性。

3. 有序性

有序性是指:在本線程內(nèi)觀察,所有操作都是有序的。在一個(gè)線程觀察另一個(gè)線程,所有操作都是無(wú)序的,無(wú)序是因?yàn)榘l(fā)生了指令重排序。在 Java 內(nèi)存模型中,允許編譯器和處理器對(duì)指令進(jìn)行重排序,重排序過(guò)程不會(huì)影響到單線程程序的執(zhí)行,卻會(huì)影響到多線程并發(fā)執(zhí)行的正確性。

volatile 關(guān)鍵字通過(guò)添加內(nèi)存屏障的方式來(lái)禁止指令重排,即重排序時(shí)不能把后面的指令放到內(nèi)存屏障之前。

也可以通過(guò) synchronized 來(lái)保證有序性,它保證每個(gè)時(shí)刻只有一個(gè)線程執(zhí)行同步代碼,相當(dāng)于是讓線程順序執(zhí)行同步代碼。

先行發(fā)生原則(Happen-Before)

JSR-133內(nèi)存模型使用先行發(fā)生原則在Java內(nèi)存模型中保證多線程操作 可見(jiàn)性 的機(jī)制,也是對(duì)早期語(yǔ)言規(guī)范中含糊的可見(jiàn)性概念的一個(gè)精確定義。上面提到了可以用 volatile 和 synchronized 來(lái)保證有序性。除此之外,JVM 還規(guī)定了先行發(fā)生原則,讓一個(gè)操作 無(wú)需控制 就能先于另一個(gè)操作完成。

由于 指令重排序 的存在,兩個(gè)操作之間有happen-before關(guān)系, 并不意味著前一個(gè)操作必須要在后一個(gè)操作之前執(zhí)行。 僅僅要求前一個(gè)操作的執(zhí)行結(jié)果對(duì)于后一個(gè)操作是可見(jiàn)的,并且前一個(gè)操作 按順序 排在第二個(gè)操作之前。

1. 單一線程原則(程序員順序規(guī)則)

Single Thread rule

在一個(gè)線程內(nèi),在程序前面的操作先行發(fā)生于后面的操作。

怎么深入理解Java內(nèi)存模型JMM

2. 管程鎖定規(guī)則(監(jiān)視器鎖規(guī)則)

Monitor Lock Rule

一個(gè) unlock(解鎖) 操作 先行發(fā)生于 后面對(duì)同一個(gè)鎖的 lock(加鎖) 操作。

怎么深入理解Java內(nèi)存模型JMM

3. volatile 變量規(guī)則

Volatile Variable Rule

對(duì)一個(gè) volatile 變量的 寫操作 先行發(fā)生于后面對(duì)這個(gè)變量的 讀操作 。

怎么深入理解Java內(nèi)存模型JMM

4. 線程啟動(dòng)規(guī)則

Thread Start Rule

Thread 對(duì)象的 start() 方法調(diào)用先行發(fā)生于此線程的每一個(gè)動(dòng)作。

怎么深入理解Java內(nèi)存模型JMM

5. 線程加入規(guī)則

Thread Join Rule

Thread 對(duì)象的結(jié)束先行發(fā)生于 join() 方法返回。

怎么深入理解Java內(nèi)存模型JMM

6. 線程中斷規(guī)則

Thread Interruption Rule

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

7. 對(duì)象終結(jié)規(guī)則

Finalizer Rule

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

8. 傳遞性

Transitivity

如果操作 A 先行發(fā)生于操作 B,操作 B 先行發(fā)生于操作 C,那么操作 A 先行發(fā)生于操作 C。

上述就是小編為大家分享的怎么深入理解Java內(nèi)存模型JMM了,如果剛好有類似的疑惑,不妨參照上述分析進(jìn)行理解。如果想知道更多相關(guān)知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI