深入理解 Java 內(nèi)存模型 JMM 與 volatile

Java 內(nèi)存模型(Java Memory Model,簡(jiǎn)稱 JMM)是一種抽象的概念,并不真實(shí)存在,它描述的是一組規(guī)范或者規(guī)則,通過(guò)這種規(guī)范定義了程序中各個(gè)變量(包括實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素)的訪問(wèn)方式。

JMM 中的主內(nèi)存和工作內(nèi)存

由于代碼運(yùn)行的實(shí)體是線程,而 JVM 會(huì)為每一個(gè)線程創(chuàng)建一個(gè)工作內(nèi)存(有些資料稱為??臻g),用于存儲(chǔ)線程私有的數(shù)據(jù)。而 Java 內(nèi)存模型規(guī)定所有變量都存儲(chǔ)在主內(nèi)存中。主內(nèi)存是共享內(nèi)存區(qū)域,所有線程都可以訪問(wèn)。但是線程不能直接操作主內(nèi)存中的變量,線程對(duì)變量的讀取和修改等操作必須在自己的工作內(nèi)存中進(jìn)行,首先從主內(nèi)存中把變量拷貝到線程私有的工作內(nèi)存,對(duì)變量進(jìn)行操作后,再將變量寫(xiě)回主內(nèi)存。線程之間的傳值必須通過(guò)主內(nèi)存完成。
<div align="center"> <img src="https://user-gold-cdn.xitu.io/2020/4/7/171554d44b4eba79?w=823&h=446&f=png&s=29990"/> </div>

JMM 中的主內(nèi)存

JMM 中的主內(nèi)存存儲(chǔ) Java 實(shí)例對(duì)象,包括成員變量、類信息、常量、靜態(tài)變量等。主內(nèi)存屬于數(shù)據(jù)共享區(qū)域,多線程并發(fā)操作時(shí)會(huì)引發(fā)線程安全問(wèn)題

JMM 中的工作內(nèi)存

  • 存儲(chǔ)當(dāng)前方法的所有本地變量信息,每個(gè)線程只能訪問(wèn)自己的工作內(nèi)存,每個(gè)線程工作內(nèi)存的本地變量對(duì)其他線程不可見(jiàn)
  • 字節(jié)碼行號(hào)指示器、Native 方法等信息
  • 屬于線程私有數(shù)據(jù)區(qū)域,不存在線程安全問(wèn)題

JMM 與 Java 內(nèi)存區(qū)域的劃分是不同的概念層次

JMM 描述的是一組規(guī)則,通過(guò)這組規(guī)則控制程序中各個(gè)變量在主內(nèi)存和工作內(nèi)存訪問(wèn)方式,圍繞原子性、有序性、可見(jiàn)性展開(kāi)。

JMM 與 Java 內(nèi)存區(qū)域劃分的相似點(diǎn)是都存在共享區(qū)域和私有區(qū)域。
JMM 中的主內(nèi)存屬于共享數(shù)據(jù)區(qū)域,應(yīng)該包括
Java 內(nèi)存區(qū)域中的堆和方法區(qū);JMM 中的工作內(nèi)存屬于私有數(shù)據(jù)區(qū)域,應(yīng)該包括
Java 內(nèi)存區(qū)域中的程序計(jì)數(shù)器、虛擬機(jī)棧和本地方法棧。

主內(nèi)存與工作內(nèi)存的數(shù)據(jù)存儲(chǔ)類型以及操作方式歸納

  • 對(duì)于實(shí)例對(duì)象中的成員方法,方法里的基本數(shù)據(jù)類型的局部變量將直接存儲(chǔ)在工作內(nèi)存的棧幀結(jié)構(gòu)中。方法里引用類型的局部變量的引用在工作內(nèi)存中的棧幀結(jié)構(gòu)中,對(duì)象實(shí)例存儲(chǔ)在主內(nèi)存(堆)中。
  • 對(duì)于實(shí)例數(shù)據(jù)的成員變量、靜態(tài)變量、類信息都會(huì)被存儲(chǔ)在主內(nèi)存中
  • 需要注意的是,在主內(nèi)存中的實(shí)例對(duì)象可以被多個(gè)線程共享,如果兩個(gè)線程調(diào)用了同一個(gè)對(duì)象的同一個(gè)方法,兩個(gè)線程會(huì)將數(shù)據(jù)拷貝到自己的工作內(nèi)存中,執(zhí)行完成后刷新回主內(nèi)存。

JMM 如何解決可見(jiàn)性問(wèn)題

忽略硬件中其他復(fù)雜的因素,上面的主內(nèi)存與工作內(nèi)存執(zhí)行方式可以理解為
把數(shù)據(jù)從內(nèi)存加載到CPU 的寄存器,操作完成之后再寫(xiě)回主內(nèi)存。在現(xiàn)代多核 CPU 的情況下,線程共享變量就有可能出現(xiàn)不一致,如果運(yùn)行在 CPU A 上的線程對(duì)某個(gè)變量進(jìn)行了修改,而運(yùn)行在其他 CPU 運(yùn)行的線程加載的是 CPU 緩存中的舊狀態(tài),可能導(dǎo)致數(shù)據(jù)的不一致。
在執(zhí)行程序時(shí),為了提高性能,編譯器和處理器常常會(huì)對(duì)指令重排序,但是指令重排序只能保證單線程的語(yǔ)義一致性,不能保證多線程下的語(yǔ)義一致性。多線程共享引入了復(fù)雜的數(shù)據(jù)依賴性,不管編譯器和處理器如何對(duì)指令重排序,都必須遵從數(shù)據(jù)的依賴性要求

指令重排序需要滿足的條件

  • 在單線程環(huán)境下不能改變程序運(yùn)行的結(jié)果
  • 存在數(shù)據(jù)依賴關(guān)系指令不允許重排序
    上面兩個(gè)條件可以歸結(jié)為一點(diǎn):無(wú)法通過(guò) happens-before 原則推導(dǎo)出來(lái)的,才能進(jìn)行指令重排序

happens-before 的八大原則

  • 程序次序規(guī)則:一個(gè)線程內(nèi),按照代碼順序,書(shū)寫(xiě)在前面的操作先行發(fā)生于書(shū)寫(xiě)在后面的規(guī)則。這個(gè)規(guī)則只對(duì)單線程有效。
  • 鎖定規(guī)則:一個(gè) unlock 操作先行發(fā)生于后面對(duì)同一個(gè)鎖的 lock 操作
  • volatile 變量規(guī)則:對(duì)一個(gè)變量的寫(xiě)操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作。這個(gè)規(guī)則保證了多線程共享變量的可見(jiàn)性
  • 傳遞規(guī)則:如果操作 A 先行發(fā)生于 操作 B,而操作 B 又先行發(fā)生于操作 C,那么操作 A 先行發(fā)生于操作 C
  • 線程啟動(dòng)規(guī)則:Thread 對(duì)象的 start() 方法先行發(fā)生于此線程的每一個(gè)動(dòng)作
  • 線程中斷規(guī)則:對(duì)線程 interrupted() 方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生
  • 線程終結(jié)規(guī)則:線程中的所有操作都先行發(fā)生于線程的終止檢測(cè),我們可以通過(guò) Thread.join()、Thread.isAlive() 返回值手段檢測(cè)到線程已經(jīng)終止執(zhí)行
  • 對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成先行發(fā)生于該對(duì)象的 finalize() 方法的開(kāi)始

如果兩個(gè)操作不滿足上述任意一個(gè) happens-before 規(guī)則,那么這兩個(gè)操作就沒(méi)有順序的保障,JVM 可以對(duì)這兩個(gè)操作進(jìn)行重排序。
如果操作 A happens-before 操作 B,那么操作 A 在內(nèi)存上所做的操作對(duì) B 是可見(jiàn)的
happens-before 原則非常重要,它是判斷線程是否安全、數(shù)據(jù)是否存在競(jìng)爭(zhēng)的主要依據(jù)。
下面是一個(gè)分析例子:

private int value=0;
//線程 A 執(zhí)行該方法
public void write(int input){
    value=input;
}

//線程 B 執(zhí)行該方法
public int read(){
    return value;
}

線程 A 執(zhí)行 write() 方法給 value 賦值,線程 B 執(zhí)行 read() 方法讀取 value 的值。
但是這段代碼不滿足 happens-before 的八大原則,無(wú)法保證線程 A 執(zhí)行的結(jié)果對(duì)線程 B 是可見(jiàn)的。我們可以通過(guò)兩個(gè)辦法解決這個(gè)線程安全問(wèn)題。

  • 給 value 變量加上 volatile 關(guān)鍵字修飾,此時(shí)滿足 happens-before 八大原則中的 volatile 變量規(guī)則
  • 或者給 read() 和 write() 方法加上 synchronized 關(guān)鍵字,此時(shí)滿足 happens-before 八大原則中的鎖定規(guī)則

happens-before 的實(shí)現(xiàn)是依賴于內(nèi)存屏障,通過(guò)禁止某些指令重排序保證內(nèi)存可見(jiàn)性。

volatile 在并發(fā)編程中很常見(jiàn),下面來(lái)談?wù)?volatile 的內(nèi)存語(yǔ)義是如何實(shí)現(xiàn)共享變量在多線程中的可見(jiàn)性的。

volatile 是 JVM 提供的輕量級(jí)同步機(jī)制,由如下兩個(gè)作用:

  • 保證被 volatile 修飾的共享變量對(duì)所有線程總是可見(jiàn)的,當(dāng)一個(gè)線程修改了被 volatile 修飾的變量,其他線程可以立即感知到這個(gè)修改
  • 禁止指令的重排序優(yōu)化

雖然對(duì) volatile 變量的寫(xiě)操作總是能立即反映到其他線程中,但是如果對(duì) volatile 變量的運(yùn)算操作不是原子性的,那么在多線程環(huán)境中不能保證安全性,下面是一個(gè)例子:

public class VolatileVisibility {
    public static volatile int value=0;
    public static void increase(){
        value++;
    }
}

在上面的代碼中,value 變量被 volatile 修飾,對(duì) value 變量的改變會(huì)立刻反映到其他線程中。但是如果多條線程同時(shí)調(diào)用 increase() 方法時(shí),還是會(huì)出現(xiàn)線程安全問(wèn)題,因?yàn)?code>value++這個(gè)操作并不具備原子性。
我們可以使用javap指令來(lái)查看上面increase()方法的字節(jié)碼,如下:

  public static void increase();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field value:I
         3: iconst_1
         4: iadd
         5: putstatic     #2                  // Field value:I
         8: return

可以看到value++在字節(jié)碼層面是如下步驟:

  • 先通過(guò)getstatic指令把value的值拷貝到操作數(shù)棧頂
  • iconst_1指令把 1 壓入操作數(shù)棧頂,
  • iadd指令把棧頂?shù)膬蓚€(gè)數(shù)相加
  • putstatic指令把相加后的結(jié)果寫(xiě)回 value 變量中。

<div align="center"> <img src="https://user-gold-cdn.xitu.io/2020/4/7/171554d42affac38?w=857&h=429&f=png&s=4626"/> </div>

上述的操作不是原子性的,如果兩個(gè)線程同時(shí)執(zhí)行increase()方法,預(yù)料中的結(jié)果應(yīng)該是value+2。一個(gè)線程把value讀入到了自己的操作數(shù)棧中,但是還沒(méi)執(zhí)行 +1 操作,此時(shí)另一個(gè)線程也讀取了value到自己的操作數(shù)棧中進(jìn)行 +1 操作,最終兩個(gè)線程返回的結(jié)果都是value+1,引發(fā)了線程安全的問(wèn)題。因此必須使用synchronized修飾increase()方法保證線程安全,使得先獲得鎖的線程的操作happens-before于隨后獲得這個(gè)鎖的線程的操作。而且由于synchronized也可以保證操作的可見(jiàn)性,這時(shí)可以不用volatile修飾value變量。

volatile 的可見(jiàn)性

如果對(duì)volatile變量的運(yùn)算操作是原子性的。那么就可以保證該變量的線程安全,下面是一個(gè)例子

public class VolatileSafe {
    private volatile boolean shutDown;
    public void close() {
        shutDown=true;
    }

    public void doWork(){
        while (!shutDown){
            System.out.println("safe...");
        }
    }
}

在這個(gè)例子中,對(duì)boolean變量的修改是原子性的,因此對(duì)這個(gè)變量的修改對(duì)其他線程立即可見(jiàn),保證了線程安全。

對(duì) volatile 變量的修改為什么可以做到立即可見(jiàn)?

  • 當(dāng)寫(xiě)一個(gè) volatile 變量時(shí),JMM 會(huì)把對(duì)該線程對(duì)應(yīng)的工作內(nèi)存中的共享變量值刷新到主內(nèi)存中
  • 當(dāng)讀取一個(gè) volatile 變量時(shí),JMM 會(huì)把該線程對(duì)應(yīng)的工作內(nèi)存置為無(wú)效,使得線程只能從主內(nèi)存中重新讀取共享變量

volatile 是通過(guò)內(nèi)存屏障來(lái)禁止指令重排序優(yōu)化的。
內(nèi)存屏障的作用有以下兩點(diǎn):

  • 通過(guò)插入內(nèi)存屏障(Memory Barrier)指令禁止對(duì)內(nèi)存屏障前后的指令執(zhí)行重排序優(yōu)化,保證特定操作的執(zhí)行順序
  • 強(qiáng)制刷出各種 CPU 的緩存數(shù)據(jù),因此在任何 CPU 上都能看到這些數(shù)據(jù)的最新版本,保證某些變量的內(nèi)存可見(jiàn)性

下面來(lái)分析一個(gè)帶有隱患的常見(jiàn)的單例寫(xiě)法:

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        //第一次檢測(cè)
        if (instance == null) {
            //同步
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

一個(gè)對(duì)象的初始化不是原子性的操作,可以分為 3 步:

  • 1.分配內(nèi)存空間
  • 2.初始化對(duì)象
  • 3.設(shè)置 instance 指向分配對(duì)象的內(nèi)存地址

上述流程可能經(jīng)過(guò)重排序。變?yōu)槿缦马樞颍?/p>

  • 1.分配內(nèi)存空間
  • 2.設(shè)置 instance 指向分配對(duì)象的內(nèi)存地址,但是對(duì)象還沒(méi)初始化,但此時(shí) instance != null
  • 3.初始化對(duì)象

我們假設(shè)線程 A 先執(zhí)行 getInstance() 方法,當(dāng)執(zhí)行完指令 2 時(shí)恰好發(fā)生了線程切換,切換到了線程 B 上;如果此時(shí)線程 B 也執(zhí)行 getInstance() 方法,那么線程 B 在執(zhí)行第一個(gè)判斷時(shí)會(huì)發(fā)現(xiàn) instance != null,所以直接返回 instance,而此時(shí)的 instance 是沒(méi)有初始化過(guò)的,如果我們這個(gè)時(shí)候訪問(wèn) instance 的成員變量就可能觸發(fā)空指針異常。
解決方法是使用volatile修飾instance變量,禁止指令重排序即可。

volatilesynchronized的區(qū)別

  • volatile 本質(zhì)是在告訴 JVM 當(dāng)前變量在寄存器(工作內(nèi)存)中的值是不確定的,需要從主存中讀取;synchronized 這是鎖定當(dāng)前變量,只有當(dāng)前線程可以訪問(wèn)該變量,其他線程被阻塞住,直到該線程完成變量操作為止。
  • volatile 僅能用在變量級(jí)別;synchronized 則可以用在變量、方法和類級(jí)別
  • volatile 僅能實(shí)現(xiàn)變量的修改可見(jiàn)性,不能保證原子性;而 synchronized 則可以保證變量修改的可見(jiàn)性和原子性
  • volatile 不會(huì)造成線程的阻塞;synchronized 可能會(huì)造成線程的阻塞
  • volatile 標(biāo)記的變量不會(huì)被編譯器優(yōu)化;synchronized 標(biāo)記的變量可能會(huì)被編譯器優(yōu)化


如果你覺(jué)得這篇文章對(duì)你有幫助,不妨點(diǎn)個(gè)贊,讓我有更多動(dòng)力寫(xiě)出好文章。

我的文章會(huì)首發(fā)在公眾號(hào)上,歡迎掃碼關(guān)注我的公眾號(hào)張賢同學(xué)。
<div align="center"><img src="https://image.zhangxiann.com/QRcode_8cm.jpg"/></div>

最后編輯于
?著作權(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)容