【知識積累】深入理解Java內存模型(JMM)

一、什麼是JMM?

java內存模型(即java memory model,簡稱JMM),本身是一種抽象的概念,並不真實存在,它描述的是一組規則或規範,通過這組規範定義了程序中各個變量(包括實例字段,靜態字段和構成數組對象的元素)的訪問方式。

二、概念區分

JVM內存模型:描述的是多線程允許的行爲。
JVM內存結構:描述的是線程運行所設計的內存空間。

建議大家研讀《深入理解Java虛擬機(第2版)》

三、抽象示意圖

由於JVM運行實例的實體是線程,而每個線程創建時都會爲其創建一個工作內存,有些地方稱爲棧空間,用於存儲線程私有的數據。而java內存模型中,規定所有變量都存儲在主內存中,主內存是共享內存區域,所有線程都可以訪問,但線程對變量的操作(即讀取賦值等),必須在工作內存中進行。

首先將變量從主內存拷貝到自己的工作內存當中,然後對變量進行操作,操作完成後再將變量寫回主內存,不能直接操作主內存中的變量。工作內存中存儲着主內存的變量的副本拷貝,工作內存是每個線程的私有區域,因此不同線程間無法訪問對方的工作內存,線程間的通信,也就是傳值必須通過主內存來完成

四、JMM的主內存

1、所有線程創建的實例對象都存放在主內存中

2、包括成員變量、類信息、常量、靜態變量等

3、由於是共享數據區域,多個線程對同一個變量進行訪問時,可能會引發線程安全問題

五、JMM的工作內存

1、主要存儲當前方法的所有本地變量信息,其實工作內存存儲的是主內存中的變量副本的拷貝,每個線程只能訪問自己的工作內存,即線程中的本地變量對其他線程不可見的,就算兩個線程執行的是同一段代碼,它們也會各自在自己的工作內存中創建屬於當前線程的本地變量。

2、字節碼行號指示器、Native方法信息

3、由於工作線程是每個線程的私有數據,線程間無法相互訪問工作內存,因此存儲在工作內存的數據,不存在線程的安全問題

六、主內存與工作內存的數據存儲類型以及操作方式歸納

1、根據虛擬機規範,對於一個實例對象中的成員方法而言,如果方法中包含本地變量是基本數據類型的,比如說blooean、byte、short、char、int、long、float、double等,這些本地變量將直接存在在工作內存的棧幀結構中;

2、但倘若本地變量是引用類型的,那該變量的引用會存儲在工作內存的棧幀中,而對象實例將存儲在主內存,即數據共享區域堆當中;

3、但對於實例對象的成員變量(不管是基本數據類型或者包裝類型,還是引用類型)、static變量、類信息均會被存儲在主內存中;

4、在主內存中的實例對象,可以被多線程共享,倘若兩個線程同時調用了同一個線程的同一個方法,那麼兩個線程會將要操作的數據拷貝一份到自己的工作內存中,執行完成操作之後,才刷新到主內存裏面。

七、JMM如何解決可見性問題

把數據從內存加載到緩存、寄存器,然後運算結束,然後寫回主內存。

如果處理器對某個變量進行了修改,可能只是體現在該內核的緩存裏,這是個本地狀態,而運行在其他內核上的線程,可能加載的是舊狀態,這很可能導致一致性的問題,從理論上說,多線程共享引入了複雜的數據依賴性,不管編譯器、處理器怎麼做、重排序都必須尊重數據依賴性的要求,否則就打破了數據的正確性,這就是JMM所要解決的問題。

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

在執行程序的時候,爲了提高性能,處理器和編譯器常常會對指令進行重排序,但是不能隨意重排序,即不是想怎麼排序就怎麼排序,它需要滿足一下兩個條件:

  • 但單線程環境下不能改變程序運行的結果
  • 存在數據依賴關係的不允許重排序

其實這兩點可以歸結爲一點:
無法通過happens-before原則推導出來的,才能進行指令的重排序。

JMM內部的實現通常是依賴於所謂的內存屏障,通過禁止某些重排序的方式,提供內存可見性保證,也就是實現了各種happens-before的規則。與此同時,更多的複雜度在於需要儘量確保各種編譯器、各種體系結構的處理器能夠提供一致的行爲。

在JMM中,如果一個操作執行的結果需要對另外一個操作可見,那麼這兩個操作之間必須存在happends-before的關係。

happends-before原則非常重要,它是判斷數據是否存在競爭、線程是否安全的主要依據。依靠這個原則,我們便能解決在併發環境下,兩個操作之間存在衝突的問題。

九、happens-before的八大原則

1、程序次序規則

一段代碼在單線程中執行的結果,是有序的。注意是執行結果是有序的,因爲虛擬機處理器會對指令重排序,雖然重排序了,但並不會影響程序的執行結果,所以程序最終執行的結果與順序執行的結果是一致的,故而這個規則只對單線程有效,在多線程環境下就無法保證準確性了;

2、鎖定規則

無論是單線程環境還是多線程環境中,一個鎖處於被鎖定的狀態,那麼必須執行unlock操作,後面才能進行lock操作;

3、votile變量規則

這是一條比較重要的規則,它標誌着volatile保證了線程的可見性,通俗的講,如果線程先去寫一個volatile的變量,然後線程去讀這個變量,那麼這個寫操作一定是happends-before這個讀操作;

4、傳遞規則

如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C;

5、線程啓動規則

Thread對象的start()方法先行發生於此線程的每一個動作;

6、線程中斷規則

假設兩個線程A和B,A先做了一些操作(即operation A),然後調用B線程的interrupte方法,當B線程感知到自己的中斷標識被設置的時候,operation A中的操作結果對B是可見的,也就是operation A調用interrupte方法,那B的標誌位就已經變化了,B也能感知的到;

7、線程終結規則

線程A在執行的過程中,通過自定Thread B.join去等待線程B終止,那麼線程B在終止之前,對共享變量的修改,在線程A等待返回後是可見的;

8、對象終結規則

結束和開始表明在時間上,一個對象的構造函數必須在它的finalize方法調用時執行完,根據這條規則,可以確保在對象的finalize方法執行時,該對象的所有filed字段值都是可見的。

十、happens-before的概念

如果兩個操作不滿足上述任意一條happens-before規則,那麼這兩個操作就沒有順序的保障,JVM可以對這兩個操作進行重排序;

如果操作A happens-before操作B,那麼操作A在內存上所做的操作對操作B都是可見的。

舉例:

    private int value = 0;

    public int read(){
        return value;
    }

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

如果線程A執行write方法,而線程B執行read方法,且線程A優先於線程B執行,那麼線程B獲得的結果是什麼呢?

我們開始分析happens-before八大原則,5、6、7、8不滿足,因爲無關;1不滿足,因爲是多線程;2不滿足,因爲沒有使用鎖;3不滿足,因爲value沒有使用volatile修飾;4不滿足,不存在傳遞。

所以我們無法通過happens-before原則推導出A happens-before B。所以這段代碼是線程不安全的。

如何解決?

滿足規則2和規則3其中一個即可,即使用synchronize或者volatile。

    private volatile int value = 0;

    public int read(){
        return value;
    }

    public void write(int value){
        this.value = value;
    }
    private int value = 0;

    public int read(){
        return value;
    }

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章