Java內存模型-jsr133規範介紹

最近在看《深入理解Java虛擬機:JVM高級特性與最佳實踐》講到了線程相關的細節知識,裏面講述了關於java內存模型,也就是jsr 133定義的規範。

系統的看了jsr 133規範的前面幾個章節的內容,覺得受益匪淺。廢話不說,簡要的介紹一下java內存規範。

什麼是內存規範

在jsr-133中是這麼定義的

A memory model describes, given a program and an execution trace of that program, whether
the execution trace is a legal execution of the program. For the Java programming language, the
memory model works by examining each read in an execution trace and checking that the write
observed by that read is valid according to certain rules.

也就是說一個內存模型描述了一個給定的程序和和它的執行路徑是否一個合法的執行路徑。對於java序言來說,內存模型通過考察在程序執行路徑中每一個讀操作,根據特定的規則,檢查寫操作對應的讀操作是否能是有效的。

java內存模型只是定義了一個規範,具體的實現可以是根據實際情況自由實現的。但是實現要滿足java內存模型定義的規範。

 

處理器和內存的交互

這個要感謝硅工業的發展,導致目前處理器的性能越來越強大。目前市場上基本上都是多核處理器。如何利用多核處理器執行程序的優勢,使得程序性能得到極大的提升,是目前來說最重要的。

目前所有的運算都是處理器來執行的,我們在大學的時候就學習過一個基本概念  程序 = 數據 + 算法 ,那麼處理器負責計算,數據從哪裏獲取了?

數據可以存放在處理器寄存器裏面(目前x86處理都是基於寄存器架構的),處理器緩存裏面,內存,磁盤,光驅等。處理器訪問這些數據的速度從快到慢依次爲:寄存器,處理器緩存,內存,磁盤,光驅。爲了加快程序運行速度,數據離處理器越近越好。但是寄存器,處理器緩存都是處理器私有數據,只有內存,磁盤,光驅纔是纔是所有處理器都可以訪問的全局數據(磁盤和光驅我們這裏不討論,只討論內存)如果程序是多線程的,那麼不同的線程可能分配到不同的處理器來執行,這些處理器需要把數據從主內存加載到處理器緩存和寄存器裏面纔可以執行(這個大學操作系統概念裏面有介紹),數據執行完成之後,在把執行結果同步到主內存。如果這些數據是所有線程共享的,那麼就會發生同步問題。處理器需要解決何時同步主內存數據,以及處理執行結果何時同步到主內存,因爲同一個處理器可能會先把數據放在處理器緩存裏面,以便程序後續繼續對數據進行操作。所以對於內存數據,由於多處理器的情況,會變的很複雜。下面是一個例子:

 

初始值 a = b = 0

process1           process2

1:load a               5:load b

2:write a:2           6:add b:1

3:load b               7: load a

4:write b:1           8:write a:1

假設處理器1先加載內存變量a,寫入a的值爲2,然後加載b,寫入b的值爲1,同時 處理2先加載b,執行b+1,那麼b在處理器2的結果可能是1 可能是3。因爲在load b之前,不知道處理器1是否已經吧b寫會到主內存。對於a來說,假設處理器1後於處理器2把a寫會到主內存,那麼a的值則爲2。

而內存模型就是規定了一個規則,處理器如何同主內存同步數據的一個規則。

內存模型介紹

在介紹java內存模型之前,我們先看看兩個內存模型

Sequential Consistency Memory Model:連續一致性模型。這個模型定義了程序執行的順序和代碼執行的順序是一致的。也就是說 如果兩個線程,一個線程T1對共享變量A進行寫操作,另外一個線程T2對A進行讀操作。如果線程T1在時間上先於T2執行,那麼T2就可以看見T1修改之後的值。

這個內存模型比較簡單,也比較直觀,比較符合現實世界的邏輯。但是這個模型定義比較嚴格,在多處理器併發執行程序的時候,會嚴重的影響程序的性能。因爲每次對共享變量的修改都要立刻同步會主內存,不能把變量保存到處理器寄存器裏面或者處理器緩存裏面。導致頻繁的讀寫內存影響性能。

 

Happens-Before Memory Model : 先行發生模型。這個模型理解起來就比較困難。先介紹一個現行發生關係 (Happens-Before Relationship

  如果有兩個操作A和B存在A Happens-Before B,那麼操作A對變量的修改對操作B來說是可見的。這個現行並不是代碼執行時間上的先後關係,而是保證執行結果是順序的。看下面例子來說明現行發生

A,B爲共享變量,r1,r2爲局部變量
 
初始 A=B=0
 
Thread1   | Thread2
1: r2=A   | 3: r1=B
2: B=2    4: A=2

  憑藉直觀感覺,線程1先執行 r2=A,則r2=0 ,然後賦值B=1,線程2執行r1=B,由於線程1修改了B的值爲1,所以r1=1。但是在現行發生內存模型裏面,有可能最終結果爲r1 = r2 = 2。爲什麼會這樣,因爲編譯器或者多處理器可能對指令進行亂序執行,線程1 從代碼流上面看是先執行r2 = A,B = 1,但是處理器執行的時候會先執行 B = 2 ,在執行 r2 = A,線程2 可能先執行 A = 2 ,在執行r1 = B,這樣可能 會導致 r1 = r2 = 2。

那我們先看看先行發生關係的規則

  • 1 在同一個線程裏面,按照代碼執行的順序(也就是代碼語義的順序),前一個操作先於後面一個操作發生
  • 2 對一個monitor對象的解鎖操作先於後續對同一個monitor對象的鎖操作
  • 3 對volatile字段的寫操作先於後面的對此字段的讀操作
  • 4 對線程的start操作(調用線程對象的start()方法)先於這個線程的其他任何操作
  • 5 一個線程中所有的操作先於其他任何線程在此線程上調用 join()方法
  • 6 如果A操作優先於B,B操作優先於C,那麼A操作優先於C

解釋一下以上幾個先行發生規則的含義

規則1應該比較好理解,因爲比較適合人正常的思維。比如在同一個線程t裏面,代碼的順序如下:

thread 1
共享變量A、B
局部變量r1、r2
 
代碼順序
1: A =1
2: r1 = A
3: B = 2
4: r2 = B
 
執行結果 就是 A=1 ,B=2 ,r1=1 ,r2=2

因爲以上是在同一個線程裏面,按照規則1 也就是按照代碼順序,A = 1 先行發生 r1 =A ,那麼r1 = 1

再看規則2,下面是jsr133的例子

按照規則2,由於unlock操作先於發生於lock操作,所以X=1對線程2裏面就是可見的,所以r2 = 1

在分析以下,看這個例子,由於unlock操作先於lock操作,所以線程x=1對於線程2不一定是可見(不一定是現行發生的),所以r2的值不一定是1,有可能是x賦值爲1之前的那個狀態值(假設x初始值爲0,那麼此時r2的值可能爲0)

對於規則3,我們可以稍微修改一下我們說明的第一個例子

A,B爲共享變量,並且B是valotile類型的
r1,r2爲局部變量
 
初始 A=B=0
 
Thread1   | Thread2
1: r2=A   | 3: r1=B
2: B=2    4: A=2
 
那麼r1 = 2, r2可能爲0或者2

 因爲對於volatile類型的變量B,線程1對B的更新馬上線程2就是可見的,所以r1的值就是確定的。由於A是非valotile類型的,所以值不確定。

規則4,5,6這裏就不解釋了,知道規則就可以了。

 可以從以上的看出,先行發生的規則有很大的靈活性,編譯器可以對指令進行重新排序,以便滿足處理器性能的需要。只要重新排序之後的結果,在單一線程裏面執行結果是可見的(也就是在同一個線程裏面滿足先行發生原則1就可以了)。

java內存模型是建立在先行發生的內存模型之上的,並且再此基礎上,增強了一些。因爲現行發生是一個弱約束的內存模型,在多線程競爭訪問共享數據的時候,會導致不可預期的結果。有一些是java內存模型可以接受的,有一些是java內存模型不可以接受的。具體細節這裏面就不詳細說明了。這裏只說明關於java新的內存模型重要點。

final字段的語義

在java裏面,如果一個類定義了一個final屬性,那麼這個屬性在初始化之後就不可以在改變。一般認爲final字段是不變的。在java內存模型裏面,對final有一個特殊的處理。如果一個類C定義了一個非static的final屬性A,以及非static final屬性B,在C的構造器裏面對A,B進行初始化,如果一個線程T1創建了類C的一個對象co,同一時刻線程T2訪問co對象的A和B屬性,如果t2獲取到已經構造完成的co對象,那麼屬性A的值是可以確定的,屬性B的值可能還未初始化,

下面一段代碼演示了這個情況

public class FinalVarClass {
 
    public final int a ;
    public int b = 0;
     
    static FinalVarClass co;
     
    public FinalVarClass(){
        a = 1;
        b = 1;
    }
     
    //線程1創建FinalVarClass對象 co
    public static void create(){
        if(co == null){
            co = new FinalVarClass();
        }
    }
     
    //線程2訪問co對象的a,b屬性
    public static void vistor(){
        if(co != null){
            System.out.println(co.a);//這裏返回的一定是1,a一定初始化完成
            System.out.println(co.b);//這裏返回的可能是0,因爲b還未初始化完成
        }
    }
}

爲什麼會發生這種情況,原因可能是處理器對創建對象的指令進行重新排序。正常情況下,對象創建語句co = new FinalVarClass()並不是原子的,簡單來說,可以分爲幾個步驟,1 分配內存空間 2 創建空的對象 3 初始化空的對象 4 把初始化完成的對象引用指向 co ,由於這幾個步驟處理器可能併發執行,比如3,4 併發執行,所以在create操作完成之後,co不一定馬上初始化完成,所以在vistor方法的時候,b的值可能還未初始化。但是如果是final字段,必須保證在對應返回引用之前初始化完成。

volatile語義

對於volatile字段,在現行發生規則裏面已經介紹過,對volatile變量的寫操作先於對變量的讀操作。也就是說任何對volatile變量的修改,都可以在其他線程裏面反應出來。對於volatile變量的介紹可以參考 本人寫的一篇文章 《java中volatile關鍵字的含義》 裏面有詳細的介紹。

volatile在java新的內存規範裏面還加強了新的語義。在老的內存規範裏面,volatile變量與非volatile變量的順序是可以重新排序的。舉個例子

public class VolatileClass {
 
    int              x = 0;
    volatile boolean v = false;
 
    //線程1write
    public void writer() {
        x = 42;
        v = true;
    }
    //線程2 read
    public void reader() {
        if (v == true) {
            System.out.println(x);//結果可能爲0,可能爲2
        }
    }
}

 線程1先調用writer方法,對x和v進行寫操作,線程reader判斷,如果v=true,則打印x。在老的內存規範裏面,可能對v和x賦值順序發生改變,導致v的寫操作先行於x的寫操作執行,同時另外一個線程判斷v的結果,由於v的寫操作先行於v的讀操作,所以if(v==true)返回真,於是程序執行打印x,此時x不一定先行與System.out.println指令之前。所以顯示的結果可能爲0,不一定爲2

但是java新的內存模型jsr133修正了這個問題,對於volatile語義的變量,自動進行lock 和 unlock操作包圍對變量volatile的讀寫操作。那麼以上語句的順序可以表示爲

 

thread1              thread2
1 :write x=1        5:lock(m)
 
2 :lock(m)          6:read v
 
3 :write v=true     7:unlock(m)
 
4 :unlock            8 :if(v==true)
 
                     9: System.out.print(x)

 由於unlock操作先於lock操作,所以x寫操作5先於發生x的讀操作9

 

以上只是jsr規範中一些小結行的內容,由於jsr133規範定義了很多術語以及很多推論,上述只是簡單的介紹了一些比較重要的內容,具體細節可以參考jsr規範的public view :http://today.java.net/pub/a/today/2004/04/13/JSR133.html

 

發佈了10 篇原創文章 · 獲贊 8 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章