java內存模型與多線程


    現代計算機,cpu在計算的時候,並不總是從內存讀取數據,它的數據讀取順序優先級是:寄存器-高速緩存-內存,線程計算的時候,原始的數據來自內存,在計算過程中,有些數據可能被頻繁讀取,這些數據被存儲在寄存器和高速緩存中,當線程計算完后,這些緩存的數據在適當的時候應該寫回內存,當多個線程同時讀寫某個內存數據時,由於涉及數據的可見性、操作的有序性,所以就會產生多線程並發問題。

    Java作為平台無關性語言,JLS(Java語言規范)定義了一個統一的內存管理模型JMM(Java Memory Model),JMM屏蔽了底層平台內存管理細節,在多線程環境中必須解決可見性和有序性的問題。JMM規定了jvm有主內存(Main Memory)和工作內存(Working Memory) ,主內存存放程序中所有的類實例、靜態數據等變量,是多個線程共享的,而工作內存存放的是該線程從主內存中拷貝過來的變量以及訪問方法所取得的局部變量,是每個線程私有的其他線程不能訪問,每個線程對變量的操作都是以先從主內存將其拷貝到工作內存再對其進行操作的方式進行,多個線程之間不能直接互相傳遞數據通信,只能通過共享變量來進行。

    dc9d7w83_47gnxqbmd8_b

    JLS定義了線程對主存的操作指令:read,load,use,assign,store,write。這些行為是不可分解的原子操作,在使用上相互依賴,read-load從主內存復制變量到當前工作內存,use-assign執行代碼改變共享變量值,store-write用工作內存數據刷新主存相關內容。

    線程要引用某變量,如果線程工作內存中沒有該個變量,通過read-load從主內存中拷貝一個副本到工作內存中,完成后線程會引用該副本,當同一個線程再次引用該變量時,有可能重新從主存中獲取變量副本(read-load-use),也有可能直接引用原來的副本(use),也就是說read、load、use順序可以由jvm實現系統決定。

    線程要寫入某變量,它會將值指定給工作內存中的變量副本(assign),完成后這個變量副本會同步到主存(store-write),至於何時同步過去,即assign,store,write順序由jvm實現系統決定。

    多線程對主存的有序性操作有可能會導致並發問題,看一個例子:

public class Test{
    public int i = 0;
    public void add(){
        i++;
    }
} 

 

    前提:線程a、b使用類Test的同一個實例,執行順序1-6

  1. 線程a從主存讀取i副本x到工作內存,工作內存中x值為0
  2. 線程b從主存讀取i副本y到工作內存,工作內存中y值為0
  3. 線程a將工作內存中x加1,工作內存中x值變為1
  4. 線程a將x提交到主存,主存中i變為1
  5. 線程b將工作內存中y加1,工作內存中y值變為1
  6. 線程b將y提交到主存中,主存中i變為1   

    *單線程環境下,i進行兩次加1,結果必定是2,但多線程環境下,i進行兩次加1,結果不一定是2,這取決於上例中第2和第4步的執行順序!

    volatile是java提供的一種同步手段,只不過它是輕量級的同步,為什么這么說,因為volatile只能保證多線程的內存可見性,不能保證多線程的執行有序性。而最徹底的同步要保證有序性和可見性。當同一線程多次重復對字段賦值時,線程有可能只對工作內存中的副本進行賦值,直到最后一次賦值后才同步到主存儲區。任何被volatile修飾的變量,都不拷貝副本到工作內存,任何修改都及時寫在主存。因此對於valatile修飾的變量的修改,所有線程馬上就能看到,但是volatile不能保證對變量的修改是有序的。要使 volatile 變量提供理想的線程安全,必須同時滿足下面兩個條件:

    1、對變量的寫操作不依賴於當前值。

    2、該變量沒有包含在具有其他變量的不變式中。

    錯誤的例子:

public class VolatileFalse{   
    public volatile int i;
    public void add(){
        i++;
    }
}

    說明:雖然volatile 保證對i的修改“及時”寫在主存,所有線程馬上能看到,但i = i + 1 對變量i的寫操作依賴於當前值,而當前值是可變的,由於多線程下讀寫i的值是無序的,所以多個線程運行VolatileFalse的同一個實例后的到i的最終值不一定是正確。

    正確的例子:

public class VolatileTrue{
    public volatile int i;
    public void setI(int j){
        this.i = j;
    }
} 

    說明:沒有volatie聲明,在多線程環境下,i的值不一定是正確的,因為this.i = j;涉及給i賦值和將i的值同步主存的步驟,這個順序可能被打亂。如果用volatie聲明了,讀取主存副本到工作內存和同步i到主存的步驟,相當於是一個原子操作,因此是線程安全的。

    volatile適合這種場景:一個變量被多個線程共享,線程直接給這個變量賦值。這是一種很簡單的同步場景,這時候使用volatile的開銷將會非常小。

    synchronized關鍵字作為多線程並發環境的執行有序性的保證手段之一,如果某個線程訪問一個標識為synchronized的方法,並對相應變量做操作,那么根據JLS,JVM的執行步驟如下:

  1. 取得該對象鎖(普通方法的鎖為this對象,靜態方法則為該類的class對象)並將其鎖住(lock)。
  2. 將需要的數據從主內存拷貝到自己的工作內存(read and load)。
  3. 根據程序流程讀取或者修改相應變量值(use and assign)。
  4. 將自己工作內存中修改了值的變量拷貝回主內存(store and write)。
  5. 釋放對象鎖(unlock)。

    對synchronized的一些總結:

public class Test{
    public void method0(){...} 
    public synchronized void method1(){...} 
    public void method2() { 
        synchronized (this){...}
    }
    public void method3(SomeObject so) { 
      synchronized(so) {...}
    } 
    public void method4() { 
      ...
      private byte[] lock = new byte[0]; 
      synchronized (lock){...} 
      ...
    }
    public synchronized static void method5(){...}
    public void method6(){
        synchronized(Test.class){...}
    } 
}
  1. method1同步函數和method2中同步塊synchronized (this){...}所取得的同步鎖都是類Test的實例對象,即對象鎖,所以method1和method2效果等同。
  2. 當多個並發線程訪問"同一個"對象中的同步函數或同步塊時,取得對象鎖的線程得到執行,該線程執行期間,其他要訪問該對象同步函數或同步塊(不管是不是相同的同步函數或同步塊)的線程將會阻塞,直到獲取該對象鎖后才能執行,當然要訪問該對象的非同步方法或同步塊的線程不受對象鎖的限制,可以直接訪問。
  3. method2同步塊synchronized (this){...}中this是指調用這個方法的對象,如果兩個線程中分別調用的是t1和t2(類Test的實例化)兩個對象,則這個同步塊對於這兩個線程來說無效,這時可以使用method3中同步塊synchronized(so) {...}方式,將鎖掛在其他對象上面。
  4. 如果要對函數中的部分代碼進行同步處理,怎么辦?method4中通過一個特別的實例變量充當鎖來實現。
  5. method5靜態同步函數和method6中同步塊synchronized(Test.class){...}所取得的同步鎖是類鎖,即類Test的鎖,而非類Test的對象鎖。
  6. 因為類鎖跟對象鎖是不同的鎖,所以在多線程並發環境下method1和method5不構成同步。 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM