jvm主內存與工作內存


一、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既保證了多線程的並發有序性,又保證了多線程的內存可見性。


免責聲明!

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



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