java內存模型與多線程

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

    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實現系統決定。併發

    多線程對主存的有序性操做有可能會致使併發問題,看一個例子: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步的執行順序!this

    volatile是java提供的一種同步手段,只不過它是 輕量級的同步,爲何這麼說,由於volatile只能保證多線程的內存可見性,不能保證多線程的執行有序性。而最完全的同步要保證有序性和可見性。當同 一線程屢次重複對字段賦值時,線程有可能只對工做內存中的副本進行賦值,直到最後一次賦值後才同步到主存儲區。任何被volatile修飾的變量,都不拷 貝副本到工做內存,任何修改都及時寫在主存。所以對於valatile修飾的變量的修改,全部線程立刻就能看到,可是volatile不能保證對變量的修 改是有序的。要使 volatile 變量提供理想的線程安全,必須同時知足下面兩個條件:spa

    一、對變量的寫操做不依賴於當前值。線程

    二、該變量沒有包含在具備其餘變量的不變式中。

    錯誤的例子:

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不構成同步。
相關文章
相關標籤/搜索