Basic Of Concurrency(五: Java內存模型)

JVM能夠看做是一個完整的計算機系統,天然會有本身的內存模型,就像物理機有RAM同樣.Java內存模型決定了Java是如何與物理機內存打交道的.若是你想編寫出具備肯定行爲的多線程併發程序,就須要對Java內存模型有必定的瞭解.jdk1.5以前的內存模型存在必定的缺陷,所以在jdk1.5以後從新發布了內存模型,這個內存模型一直沿用到jdk1.8.html

JVM中的內存模型

在Java內存模型中,將內存區域劃分爲線程棧和堆.以下圖展現了Java內存模型的邏輯平面圖:java

jmm.png

線程棧

在java中,每建立一個線程,JVM就會在內存中建立一個線程棧,該線程棧中的信息僅會被當前線程訪問,對其餘線程是不可見的.且每當線程運行時,線程棧中的信息都會獲得更新.緩存

線程棧用於存儲線程執行過程當中所有方法的所有局部變量以及線程當前執行方法的現場信息,方便用於在線程切換後恢復運行.安全

線程棧中存放的數據類型有基礎數據類型(short, int, long, float, double, boolean, byte, char等)和引用類型,而引用類型引用的具體對象則存放在堆內存中.多線程

儘管多個線程執行的是同一段代碼,他們各自在線程棧中都有一份局部變量的拷貝,互不影響,各自獨立.架構

儘管線程能夠將本身的局部變量傳遞給另外一個線程,然而其餘線程僅能獲得這個線程局部變量的拷貝,並不能訪問到這個線程局部變量自己.併發

在Java應用中建立的全部對象都會存儲在堆中,不管是哪一個線程建立的對象.這些對象包含基礎數據類型的引用類型(Integer, Long等).post

對象的成員變量會跟隨對象存儲在堆中,而在方法內建立和使用的對象也會存儲在堆中,而存儲在線程棧中的僅僅是該對象的引用.一個對象做爲另外一個對象的成員變量也同樣會存儲在堆中.this

實例

  • 局部變量中的基礎數據類型, 所有存儲在線程棧中
  • 局部變量中的引用類型,引用通常存儲在線程棧中,而引用指向的對象將存儲在堆中
  • 一個對象能夠包含若干方法,一個方法能夠包含若干局部變量.局部變量將所有存儲在線程棧中.儘管這些方法所屬的對象是存儲在堆中的.
  • 一個對象包含若干成員變量,這些變量將跟隨對象一塊兒存儲在堆中.不管這些變量是基礎數據類型仍是指向對象的引用類型.
  • 靜態對象和常量將跟隨類聲明一塊兒被存儲在堆中.
  • 只要線程中擁有堆內對象的引用就能夠訪問到堆中的所有對象.只要能訪問到特定對象,就能訪問到該對象中的成員變量.若兩個線程同時調用一個對象的方法,那麼兩個線程能同時訪問到該對象的成員變量,但兩個線程都會持有各自的局部變量拷貝.

下圖以邏輯平面圖的方式展現上文說起的情形:編碼

jmm2.png

圖中展現了兩個線程在java內存模型中的邏輯平面圖.兩個線程棧中各自有兩個方法,方法A和方法B,方法A中有兩個局部變量,方法B中有一個局部變量.其中局部變量1爲基礎類型,局部變量2爲引用類型,兩個線程棧的局部變量2都指向了堆中的對象3,其中對象3中有兩個成員變量,成員變量都爲引用類型,分別指向對象2和對象4.兩個線程棧中的局部變量3都爲引用類型,分別指向堆中的對象1和對象5.

經過編碼實例來覆蓋上文說起的情形,咱們建立一個對象JMMExample類,用於模擬上圖中存儲實例的情形.

public class JMMExample {
    public static class Object1OrObject5 {
        private String str = "obj1 or obj5";

    }

    public static class Object2 {
        private String str = "obj2";

    }

    public static class Object3 {
        public static Object3 getInstance() {
            return new Object3();
        }

        private Object2 obj2;

        private Object4 obj4;

        public Object3() {
            this.obj2 = new Object2();
            this.obj4 = new Object4();
        }

        public Object2 getObj2() {
            return obj2;
        }

        public Object4 getObj4() {
            return obj4;
        }
    }

    public static class Object4 {
        private String str = "obj4";

    }

    public void methodA(Object3 localVariable2) {
        Thread thread = Thread.currentThread();

        System.out.println(thread.getName() + " using localVariable2( " + localVariable2 + " )");

        int localVariable1 = 10;
        System.out.println(thread.getName() + " using localVariable1( " + localVariable1 + " )");

        Object2 obj2 = localVariable2.getObj2();
        Object4 obj4 = localVariable2.getObj4();
        System.out.println(thread.getName() + " using localVariable2 point to ( " + obj2 + " )");
        System.out.println(thread.getName() + " using localVariable2 point to ( " + obj4 + " )");

        methodB();
    }

    public void methodB() {
        Thread thread = Thread.currentThread();

        Object1OrObject5 obj = new Object1OrObject5();
        System.out.println(thread.getName() + " using localVariable3 point to ( " + obj + " )");
    }

    public static void main(String[] args) {
        final Object3 obj3 = Object3.getInstance();
        final JMMExample jmmExample = new JMMExample();
        Runnable myRunnable = () -> jmmExample.methodA(obj3);

        IntStream.range(1, 3)
                .forEach(i -> new Thread(myRunnable, "Thread-" + i).start());
    }
}
複製代碼

執行結果:

Thread-1 using localVariable2( org.menfre.JMMExample$Object3@41d5399c )
Thread-1 using localVariable1( 10 )
Thread-2 using localVariable2( org.menfre.JMMExample$Object3@41d5399c )
Thread-2 using localVariable1( 10 )
Thread-2 using localVariable2 point to ( org.menfre.JMMExample$Object2@4f12cfd6 )
Thread-1 using localVariable2 point to ( org.menfre.JMMExample$Object2@4f12cfd6 )
Thread-1 using localVariable2 point to ( org.menfre.JMMExample$Object4@7c716752 )
Thread-2 using localVariable2 point to ( org.menfre.JMMExample$Object4@7c716752 )
Thread-1 using localVariable3 point to ( org.menfre.JMMExample$Object1OrObject5@4b2d52ec )
Thread-2 using localVariable3 point to ( org.menfre.JMMExample$Object1OrObject5@72b696ac )

從結果咱們能夠看出線程1和線程2各自將局部變量1和2加載到線程棧中,其中局部變量1爲int類型,數值爲10,局部變量2爲引用類型,指向對象Object3,從運行結果看出他們指向的對象爲同一個.隨後線程1和線程2經過Object3的成員變量訪問到Object2和Object4,從運行結果能夠看出Object2和Object4也爲同一個對象.再而後線程1和2分別建立了各自的局部變量3,從運行結果能夠看出兩個線程的局部變量3指向的對象是不一樣的,這符合上文說起的Object1和Object5.

物理機內存架構

物理機內存架構跟java內存模型不太同樣,但瞭解物理機內存架構有助於理解java內存模型與物理機內存之間的交互。

物理機內存架構邏輯平面圖以下:

jmm3.png

現代計算機一般擁有兩個或兩個以上的cpu數量,有些cpu甚至擁有多個核心。這使得多個線程同時執行成爲可能。在同一時間點,每一個線程能夠交由一個cpu進行調度,多個線程能夠同時執行。若你的應用支持多線程,那麼你的程序將會在多個cpu中執行。

一般物理機內存會有三層架構,分別是主存,cpu緩存(cpu緩存可能會有多級,如1~3不等,但不影響理解),還有位於cpu中的多組寄存器。主存的容量通常比cpu緩存和寄存器的容量大得多,而cpu緩存容量會比寄存器大。

一般寄存器的讀寫速度會大於cpu緩存,而cpu緩存的讀寫速度會大於主存。cpu執行時會將主存的部分數據加載到cpu緩存,一樣會將cpu緩存中的部分數據加載到寄存器中,而後再對寄存器中的數值進行操做。

cpu執行結束後會將結果回寫到cpu緩存,但cpu緩存中的數據更新後不會當即寫回到主存中,而是當cpu須要將其餘數據加載到cpu緩存中時,纔將cpu緩存中更新後的數據回寫到主存。

cpu緩存不會一次性的寫入和寫出整個緩存區,而是分塊的進行寫入和寫出。cpu緩存中分塊單位稱爲「cache lines」。

Java內存模型與物理機內存架構交互

物理機內存並不會區分Java內存模型中的線程棧和堆。會將線程棧中的局部變量和堆中的對象無差異的載入主存中。且不管是線程棧中的局部變量仍是堆中的對象都會出如今物理機內存三層架構中。以下邏輯平面圖所示:

jmm4.png

一旦變量和對象被載入到多個存儲區域後,就會暴露出特定的問題,最主要的問題是以下兩種;

  • 共享對象更改後對其餘線程的可見性
  • 多線程讀取和更改共享變量時產生的竟態條件

共享對象可見性

若一個線程將主存中的共享變量加載到cpu緩存中操做,隨後另外一個線程將主存中的共享變量加載到cpu緩存中,此時第一個線程在cpu緩存中對共享變量進行的更新對另外一個線程不可見。以下邏輯平面圖所示:

jmm5.png

圖中主存中存儲有共享變量count,count值等於1。在沒有volatile修飾符修飾的狀況下,此時兩個線程分別將共享變量加載到cpu緩存中,第一個線程對count進行+1操做,此時count數值更新爲2,但cpu緩存並不會將更新後的數值當即寫回主存,此時線程2加載到cpu緩存中的count並非線程1更新後的數值。

java中,能夠用volatile修飾共享變量,來讓共享變量獲得更新後當即寫回主存,以解決上述問題。

竟態條件

若兩個線程同時將主存中的共享變量加載到cpu緩存中進行操做,同時更新共享變量,寫回主存後的預期變化是兩次更新都能對主存中的共享變量生效。但事實是,在沒有任何同步措施的狀況下,兩個更新被回寫到主存後,僅會保留最後一次的更新。以下邏輯平面圖所示:

jmm6.png

圖中主存中存儲有共享變量count,count值等於1。在沒有任何同步措施的狀況下,此時兩個線程同時將共享變量加載到cpu緩存中,兩個線程都對count進行+1操做,此時共享變量在cpu緩存中有兩個版本。在兩個線程將更新後的count寫回主存時,主存中的共享變量應該被更新成3,但此時僅會有一個+1操做生效,即count會被更新爲2.

java中,能夠用synchronized來修飾臨界區代碼,生成同步塊。同步塊中的變量一次僅能被一個線程訪問加載到cpu緩存中,且cpu緩存中對同步塊中的共享變量的更新會被當即寫回主存,不管該共享變量有無volatile修飾。

該系列博文爲筆者複習基礎所著譯文或理解後的產物,複習原文來自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial

上一篇: 線程安全
下一篇: 同步代碼塊

相關文章
相關標籤/搜索