1. 程式人生 > >jvm主記憶體與工作記憶體

jvm主記憶體與工作記憶體

本文轉自:https://blog.csdn.net/lovetea99/article/details/53375649

一、jvm主記憶體與工作記憶體

    首先,JVM將記憶體組織為主記憶體和工作記憶體兩個部分。

    主記憶體主要包括本地方法區和堆。每個執行緒都有一個工作記憶體,工作記憶體中主要包括兩個部分,一個是屬於該執行緒私有的棧和對主存部分變數拷貝的暫存器(包括程式計數器PC和cup工作的快取記憶體區)。  

1.所有的變數都儲存在主記憶體中(虛擬機器記憶體的一部分),對於所有執行緒都是共享的。

2.每條執行緒都有自己的工作記憶體,工作記憶體中儲存的是主存中某些變數的拷貝,執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數。
3.執行緒之間無法直接訪問對方的工作記憶體中的變數,執行緒間變數的傳遞均需要通過主記憶體來完成。

 

 

 

JVM規範定義了執行緒對記憶體間互動操作:

Lock(鎖定):作用於主記憶體中的變數,把一個變數標識為一條執行緒獨佔的狀態。

Read(讀取):作用於主記憶體中的變數,把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中。

Load(載入):作用於工作記憶體中的變數,把read操作從主記憶體中得到的變數的值放入工作記憶體的變數副本中。

Use(使用):作用於工作記憶體中的變數,把工作記憶體中一個變數的值傳遞給執行引擎。

Assign(賦值):作用於工作記憶體中的變數,把一個從執行引擎接收到的值賦值給工作記憶體中的變數。

Store(儲存):作用於工作記憶體中的變數,把工作記憶體中的一個變數的值傳送到主記憶體中。

Write(寫入):作用於主記憶體中的變數,把store操作從工作記憶體中得到的變數的值放入主記憶體的變數中。

Unlock(解鎖):作用於主記憶體中的變數,把一個處於鎖定狀態的變數釋放出來,之後可被其它執行緒鎖定。

在將變數從主記憶體讀取到工作記憶體中,必須順序執行read、load;要將變數從工作記憶體同步回主記憶體中,必須順序執行store、write。並且這8種操作必須遵循以下規則:

 

  1. 不允許read和load、store和write操作之一單獨出現。即不允許一個變數從主記憶體被讀取了,但是工作記憶體不接受,或者從工作記憶體回寫了但是主記憶體不接受。
  2.  不允許一個執行緒丟棄它最近的一個assign操作,即變數在工作記憶體被更改後必須同步改更改回主記憶體。 
  3. 工作記憶體中的變數在沒有執行過assign操作時,不允許無意義的同步回主記憶體。 
  4. 在執行use前必須已執行load,在執行store前必須已執行assign。 
  5. 一個變數在同一時刻只允許一個執行緒對其執行lock操作,一個執行緒可以對同一個變數執行多次lock,但必須執行相同次數的unlock操作才可解鎖。
  6.  一個執行緒在lock一個變數的時候,將會清空工作記憶體中的此變數的值,執行引擎在use前必須重新read和load。
  7.  執行緒不允許unlock其他執行緒的lock操作。並且unlock操作必須是在本執行緒的lock操作之後。 - 8,在執行unlock之前,必須首先執行了store和write操作。

 

下面看看上述記憶體模型與Java多執行緒之間的問題:

 

    java的多執行緒併發問題最終都會反映在java的記憶體模型上,所謂執行緒安全無非是要控制多個執行緒對某個資源的有序訪問或修改。總結java的記憶體模型,要解決兩個主要的問題:可見性和有序性。

     那麼,何謂可見性? 多個執行緒之間是不能互相傳遞資料通訊的,它們之間的溝通只能通過共享變數來進行。Java記憶體模型(JMM)規定了jvm有主記憶體,主記憶體是多個執行緒共享的。當new一個物件的時候,也是被分配在主記憶體中,每個執行緒都有自己的工作記憶體,工作記憶體儲存了主存的某些物件的副本,當然執行緒的工作記憶體大小是有限制的。當執行緒操作某個物件時,執行順序如下:
(1) 從主存複製變數到當前工作記憶體 (read and load)
(2) 執行程式碼,改變共享變數值 (use and assign)
(3) 用工作記憶體資料重新整理主存相關內容 (store and write)
當一個共享變數在多個執行緒的工作記憶體中都有副本時,如果一個執行緒修改了這個共享變數,那麼其他執行緒應該能夠看到這個被修改後的值,這就是多執行緒的可見性問題,java中volatile解決了可見性問題,接下來看一下volatile關鍵字:

volatile關鍵字 
       volatile是java提供的一種同步手段,只不過它是輕量級的同步,為什麼這麼說,因為volatile只能保證多執行緒的記憶體可見性,不能保證多執行緒的執行有序性。而最徹底的同步要保證有序性和可見性,例如synchronized。任何被volatile修飾的變數,都不拷貝副本到工作記憶體,任何修改都及時寫在主存。因此對於Valatile修飾的變數的修改,所有執行緒馬上就能看到,但是volatile不能保證對變數的修改是有序的。什麼意思呢?假如有這樣的程式碼:

Java程式碼  收藏程式碼

  1. public class Test{  
  2.   public volatile int a;  
  3.   public void add(int count){  
  4.        a=a+count;  
  5.   }  
  6. }  

 

 


        當一個Test物件被多個執行緒共享,a的值不一定是正確的,因為a=a+count包含了好幾步操作,而此時多個執行緒的執行是無序的,因為沒有任何機制來保證多個執行緒的執行有序性和原子性。volatile存在的意義是,任何執行緒對a的修改,都會馬上被其他執行緒讀取到,因為直接操作主存,沒有執行緒對工作記憶體和主存的同步。所以,volatile的使用場景是有限的,在有限的一些情形下可以使用 volatile 變數替代鎖。要使 volatile 變數提供理想的執行緒安全,必須同時滿足下面兩個條件:
1)對變數的寫操作不依賴於當前值。
2)該變數沒有包含在具有其他變數的不變式中 

volatile只保證了可見性,所以Volatile適合直接賦值的場景,如

Java程式碼  收藏程式碼

  1. public class Test{  
  2.   public volatile int a;  
  3.   public void setA(int a){  
  4.       this.a=a;  
  5.   }  
  6. }  

 
      在沒有volatile宣告時,多執行緒環境下,a的最終值不一定是正確的,因為this.a=a;涉及到給a賦值和將a同步回主存的步驟,這個順序可能被打亂。如果用volatile聲明瞭,讀取主存副本到工作記憶體和同步a到主存的步驟,相當於是一個原子操作。所以簡單來說,volatile適合這種場景:一個變數被多個執行緒共享,執行緒直接給這個變數賦值。這是一種很簡單的同步場景,這時候使用volatile的開銷將會非常小。

 

      那麼繼續說什麼是序性呢?多個執行緒執行時,CPU對執行緒的排程是隨機的,我們不知道當前程式被執行到哪步就切換到了下一個執行緒,執行緒在引用變數時不能直接從主記憶體中引用,如果執行緒工作記憶體中沒有該變數,則會從主記憶體中拷貝一個副本到工作記憶體中,這個過程為read-load,完成後執行緒會引用該副本,執行緒不能直接為主存中中欄位賦值,它會將值指定給工作記憶體中的變數副本(assign),完成後這個變數副本會同步到主儲存區(store-write),至於何時同步過去,根據JVM實現系統決定。

     這裡看一個最經典的例子就是銀行匯款問題,一個銀行賬戶存款100,這時一個人從該賬戶取10元,同時另一個人向該賬戶匯10元,那麼餘額應該還是100。那麼此時可能發生這種情況,A執行緒負責取款,B執行緒負責匯款,A從主記憶體讀到100,B從主記憶體讀到100,A執行減10操作,並將資料重新整理到主記憶體,這時主記憶體資料100-10=90,而B記憶體執行加10操作,並將資料重新整理到主記憶體,最後主記憶體資料100+10=110,顯然這是一個嚴重的問題,我們要保證A執行緒和B執行緒有序執行,先取款後匯款或者先匯款後取款。

 

這裡將一個非原子操作進行分解分步說明,假設有一個共享變數x,執行緒Thread1執行x=x+1。從上面的描述中可以知道x=x+1並不是一個原子操作,它的執行過程如下:
1 從主存中讀取變數x副本到工作記憶體
2 給x加1
3 將x加1後的值寫回主存
如果另外一個執行緒b執行x=x-1,執行過程如下:
1 從主存中讀取變數x副本到工作記憶體
2 給x減1
3 將x減1後的值寫回主存
那麼顯然,最終的x的值是不可靠的。假設x現在為10,執行緒a加1,執行緒b減1,從表面上看,似乎最終x還是為10,但是多線
程情況下會有這種情況發生:
1:執行緒a從主存讀取x副本到工作記憶體,工作記憶體中x值為10
2:執行緒b從主存讀取x副本到工作記憶體,工作記憶體中x值為10
3:執行緒a將工作記憶體中x加1,工作記憶體中x值為11
4:執行緒a將x提交主存中,主存中x為11
5:執行緒b將工作記憶體中x值減1,工作記憶體中x值為9
6:執行緒b將x提交到中主存中,主存中x為9
同樣,x有可能為11,每次執行的結果都是不確定的,因為執行緒的執行順序是不可預見的。這是java同步產生的根源,synchronized關鍵字保證了多個執行緒對於同步塊是互斥的,synchronized作為一種同步手段,解決java多執行緒的執行有序性和記憶體可見性,而volatile關鍵字之解決多執行緒的記憶體可見性問題。


synchronized關鍵字 
        上面說了,java用synchronized關鍵字做為多執行緒併發環境的執行有序性的保證手段之一。當一段程式碼會修改共享變數,這一段程式碼成為互斥區或臨界區,為了保證共享變數的正確性,synchronized標示了臨界區。典型的用法如下:

Java程式碼  

  1. synchronized(鎖){  
  2.      臨界區程式碼  
  3. }   

 


為了保證銀行賬戶的安全,可以操作賬戶的方法如下:

Java程式碼  

  1. public synchronized void add(int putMoney) {  
  2.     money = money+ putMoney;  
  3. }  
  4. public synchronized void minus(int getMoney) {  
  5.      money = money - getMoney;  
  6. }  

 


剛才不是說了synchronized的用法是這樣的嗎:

Java程式碼  

  1. synchronized(鎖){  
  2. 臨界區程式碼  
  3. }  

 


那麼對於public synchronized void add(int putMoney)這種情況,意味著什麼呢?其實這種情況,鎖就是這個方法所在的物件。同理,如果方法是public  static synchronized void add(int putMoney),那麼鎖就是這個方法所在的class。
        理論上,每個物件都可以做為鎖,但一個物件做為鎖時,應該被多個執行緒共享,這樣才顯得有意義,在併發環境下,一個沒有共享的物件作為鎖是沒有意義的。假如有這樣的程式碼:

Java程式碼  收藏程式碼

  1. public class ThreadTest{  
  2.   public void test(){  
  3.      Object lock=new Object();  
  4.      synchronized (lock){  
  5.         //do something  
  6.      }  
  7.   }  
  8. }  

 


lock變數作為一個鎖存在根本沒有意義,因為它根本不是共享物件,每個執行緒進來都會執行Object lock=new Object();每個執行緒都有自己的lock,根本不存在鎖競爭。
        每個鎖物件都有兩個佇列,一個是就緒佇列,一個是阻塞佇列,就緒佇列儲存了將要獲得鎖的執行緒,阻塞佇列儲存了被阻塞的執行緒,當一個被執行緒被喚醒(notify)後,才會進入到就緒佇列,等待cpu的排程。當一開始執行緒a第一次執行account.add方法時,jvm會檢查鎖物件account的就緒佇列是否已經有執行緒在等待,如果有則表明account的鎖已經被佔用了,由於是第一次執行,account的就緒佇列為空,所以執行緒a獲得了鎖,執行account.add方法。如果恰好在這個時候,執行緒b要執行account.minus方法,因為執行緒a已經獲得了鎖還沒有釋放,所以執行緒b要進入account的就緒佇列,等到得到鎖後才可以執行。
一個執行緒執行臨界區程式碼過程如下:
1 獲得同步鎖
2 清空工作記憶體
3 從主存拷貝變數副本到工作記憶體
4 對這些變數計算
5 將變數從工作記憶體寫回到主存
6 釋放鎖
可見,synchronized既保證了多執行緒的併發有序性,又保證了多執行緒的記憶體可見性。