jvm運行機制和volatile關鍵字詳解

參考https://www.cnblogs.com/dolphin0520/p/3920373.htmlhtml

JVM啓動流程

1.java虛擬機啓動的命令是經過java +xxx(類名,這個類中要有main方法)或者javaw啓動的。java

2.執行命令後,系統第一步作的就是裝載配置,會在當前路徑中尋找jvm的config配置文件。算法

3.找到jvm的config配置文件以後會去定位jvm.dll這個文件。這個文件就是java虛擬機的主要實現。編程

4.當找到匹配當前版本的jvm.dll文件後,就會使用這個dll去初始化jvm虛擬機。得到相關的接口。以後找到main方法開始運行。緩存

上面這個過程的描述雖然比較簡單,可是jvm的啓動流程基本都已經涵蓋在裏面了。多線程

jvm的基本結構

類加載器子系統就是一般咱們所說的ClassLoader類加載器,首先咱們會經過ClassLoader加載到jvm的內存中去,本地方法區主要就是native的方法調用,這個咱們不前不作關心,

pc寄存器

每一個線程擁有一個PC寄存器 在線程建立時 建立 指向下一條指令的地址 執行本地方法時,PC的值爲undefined併發

方法區

方法區是用來保存類的原信息。用來描述類的信息,包括類型常量池,字段方法信息,方法字節碼。在JDK6的時候字符串常量是放在方法區中,可是JDK7的時候就已經移到了堆中。因此從這方面來講方法區,堆中到底保存的是什麼信息和jdk的版本有很大的關係。從通常意義上來講咱們的方法區就是保存一些類的原信息。方法區一般和永久區(perm)關聯在一塊兒,保存一些相對穩定的數據,

java堆

1.java堆應該是和程序開發中最爲密切的一個內存區間,咱們在程序開發中經過new出來的對象基本上都是保存在java堆中。
2.堆是全局共享的,全部線程都共享java堆,也就是你建立了一個對象以後,全部的線程都是可以訪問的。
3.從GC的角度看,java堆的結構和GC的算法是有關係的。

java棧

1.java棧和堆相比是線程私有的,棧是由一系列幀組成的,因此java棧也叫做幀棧。幀中保存的內容是一個方法的局部變量,操做數棧,常量池指針。每一次方法調用都會建立一個新的幀,並壓棧。
局部變量表結構:
 
 

 

 //操做數棧,以局部變量表爲圖紙,擴充圖紙,並進行圖紙修改(局部變量的運算)。
 
 若是咱們屢次在堆上分配了對象空間,可是卻忘記了刪除對象,就會出現內存泄露,就是咱們分配空間卻沒有刪除。內存泄露在實際開發中是很是難以解決的問題,由於內存泄露有可能發生在任何地方。 
咱們能夠採用右面的方法,聲明一個對象,咱們像上面右面的方法中聲明一個對象,那麼他並無實際的劃份內存空間,而只是在java棧上產生了一個引用。而這個引用在咱們使用後會自動釋放,不會產生內存泄露的問題。
 
咱們從上面的代碼和主時中能夠交互,一個程序要想執行是須要幾個內存區域交互配合執行的。
從上面這個圖中咱們能夠發現,每一個線程讀取和存儲的都是線程的工做內存。而線程的工做內存再到主存中的存儲是確定會有一些時差的。也就是改變了一個變量的值以後,另外一個調用這個變量的對象是不能立刻知道的。若是說要讓其餘線程當即可見這個改動,就要使用volatile關鍵字修飾。一旦使用這個關鍵字以後,全部調用這個變量的線程就直接去主存當中拿取數據。每一個線程不能訪問其餘線程的工做內存。
下面這個圖就是線程和本地內存和主存之間的關係。
 
 

 

線程老是在本身的本地內存中拿取變量,而本地內存中存儲的只是共享變量的一個副本,真正的共享變量是存儲在主存中的。因此這個之間存在了必定的時延和偏差。
爲了解決緩存(這裏主要指工做內存)不一致性問題,一般來講有如下2種解決方法:

  1)經過在總線加LOCK#鎖的方式app

  2)經過緩存一致性協議jvm

  這2種方式都是硬件層面上提供的方式。函數

  在早期的CPU當中,是經過在總線上加LOCK#鎖的形式來解決緩存不一致的問題。由於CPU和其餘部件進行通訊都是經過總線來進行的,若是對總線加LOCK#鎖的話,也就是說阻塞了其餘CPU對其餘部件訪問(如內存),從而使得只能有一個CPU能使用這個變量的內存。好比上面例子中 若是一個線程在執行 i = i +1,若是在執行這段代碼的過程當中,在總線上發出了LCOK#鎖的信號,那麼只有等待這段代碼徹底執行完畢以後,其餘CPU才能從變量i所在的內存讀取變量,而後進行相應的操做。這樣就解決了緩存不一致的問題。

  可是上面的方式會有一個問題,因爲在鎖住總線期間,其餘CPU沒法訪問內存,致使效率低下。

  因此就出現了緩存一致性協議。最出名的就是Intel 的MESI協議,MESI協議保證了每一個緩存中使用的共享變量的副本是一致的。它核心的思想是:當CPU寫數據時,若是發現操做的變量是共享變量,即在其餘CPU中也存在該變量的副本,會發出信號通知其餘CPU將該變量的緩存行置爲無效狀態,所以當其餘CPU須要讀取這個變量時,發現本身緩存中緩存該變量的緩存行是無效的,那麼它就會從內存從新讀取。

可見性

 
可見性是指一個線程修改了變量以後,其餘線程可以當即知道。
保證可見性的方法就是上圖提到的三種方法。
 

有序性和指令重排

有序性:在一個線程當中,全部的指令,全部的操做都是有序的。可是在線程外觀察,在多線程的狀況下去觀察前面一個線程的行爲,咱們會發現這個行爲有可能就是無序的(這種無序有兩種緣由,一種就是指令重排,另外一種就是主存同步的延時,也就是說在線程A中更改了一個變量的值,同步主存也成功了,可是在線程B中咱們可能還沒來得及去同步主存中的值,這個時候對於線程B來講線程A的操做可能就是無序的)。

線程內串行語義 :

寫後讀 a = 1;b = a; 寫一個變量以後,再讀這個位置。

寫後寫 a = 1;a = 2; 寫一個變量以後,再寫這個變量。

讀後寫 a = b;b = 1; 讀一個變量以後,再寫這個變量。

以上語句不可重排 編譯器不考慮多線程間的語義 可重排: a=1;b=2;

 eg:
int  a =  10 ;     //語句1
int  r =  2 ;     //語句2
a = a +  3 ;     //語句3
r = a*a;      //語句4

   這段代碼有4個語句,那麼可能的一個執行順序是:

  

  

  那麼可不多是這個執行順序呢: 語句2   語句1    語句4   語句3

  不可能,由於處理器在進行重排序時是會考慮指令之間的數據依賴性,若是一個指令Instruction 2必須用到Instruction 1的結果,那麼處理器會保證Instruction 1會在Instruction 2以前執行。

  雖然重排序不會影響單個線程內程序執行的結果,可是多線程呢?下面看一個例子:

//線程1:
context = loadContext();    //語句1
inited =  true ;              //語句2
 
//線程2:
while (!inited ){
   sleep()
}
doSomethingwithconfig(context);

   上面代碼中,因爲語句1和語句2沒有數據依賴性,所以可能會被重排序。假如發生了重排序,在線程1執行過程當中先執行語句2,而此是線程2會覺得初始化工做已經完成,那麼就會跳出while循環,去執行doSomethingwithconfig(context)方法,而此時context並無被初始化,就會致使程序出錯。

   從上面能夠看出,指令重排序不會影響單個線程的執行,可是會影響到線程併發執行的正確性。

  也就是說,要想併發程序正確地執行,必需要保證併發編程特性(原子性、可見性以及有序性)。只要有一個沒有被保證,就有可能會致使程序運行不正確。

 

1.原子性

  在Java中,對基本數據類型的變量的讀取和賦值操做是原子性操做,即這些操做是不可被中斷的,要麼執行,要麼不執行。

  上面一句話雖然看起來簡單,可是理解起來並非那麼容易。看下面一個例子i:

  請分析如下哪些操做是原子性操做:

1
2
3
4
x =  10 ;          //語句1
y = x;          //語句2
x++;            //語句3
x = x +  1 ;      //語句4

   咋一看,有些朋友可能會說上面的4個語句中的操做都是原子性操做。其實只有語句1是原子性操做,其餘三個語句都不是原子性操做。

  語句1是直接將數值10賦值給x,也就是說線程執行這個語句的會直接將數值10寫入到工做內存中。

  語句2實際上包含2個操做,它先要去讀取x的值,再將x的值寫入工做內存,雖然讀取x的值以及 將x的值寫入工做內存 這2個操做都是原子性操做,可是合起來就不是原子性操做了。

  一樣的,x++和 x = x+1包括3個操做:讀取x的值,進行加1操做,寫入新的值。

   因此上面4個語句只有語句1的操做具有原子性。

  也就是說,只有簡單的讀取、賦值(並且必須是將數字賦值給某個變量,變量之間的相互賦值不是原子操做)纔是原子操做。能夠經過synchronized和Lock來實現。因爲synchronized和Lock可以保證任一時刻只有一個線程執行該代碼塊,那麼天然就不存在原子性問題了,從而保證了原子性。

補充:  volatile不能保證原子性
volatile關鍵字保證了操做的可見性,可是volatile不能保證操做是原子性
下面看一個例子:
public class Test {
    public volatile int inc = 0;
     
    public void increase() {
        inc++;
    }
     
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
         
        while(Thread.activeCount()>1)  //保證前面的線程都執行完
            Thread.yield();
        System.out.println(test.inc);
    }
}
   你們想一下這段程序的輸出結果是多少?也許有些朋友認爲是10000。可是事實上運行它會發現每次運行結果都不一致,都是一個小於10000的數字。

  上面是對變量inc進行自增操做,因爲volatile保證了可見性,那麼在每一個線程中對inc自增完以後,在其餘線程中都能看到修改後的值啊,因此有10個線程分別進行了1000次操做,
  那麼最終inc的值應該是1000
*10=10000。   這裏面就有一個誤區了,volatile關鍵字能保證可見性沒有錯,可是上面的程序錯在沒能保證原子性。可見性只能保證每次讀取的是最新的值,可是volatile沒辦法保證對變量的
  操做的原子性。   在前面已經提到過,自增操做是不具有原子性的,它包括讀取變量的原始值、進行加1操做、寫入工做內存。那麼就是說自增操做的三個子操做可能會分割開執行,
  就有可能致使下面這種狀況出現:   假如某個時刻變量inc的值爲10,   線程1對變量進行自增操做,線程1先讀取了變量inc的原始值,而後線程1被阻塞了   而後線程2對變量進行自增操做,線程2也去讀取變量inc的原始值,因爲線程1只是對變量inc進行讀取操做,而沒有對變量進行修改操做,
  因此不會致使線程2的工做內存中緩存變量inc的緩存行無效,
  因此線程2會直接去主存讀取inc的值,發現inc的值時10,
  而後進行加1操做,並把11寫入工做內存,最後寫入主存。   而後線程1接着進行加1操做,因爲已經讀取了inc的值,注意此時在線程1的工做內存中inc的值仍然爲10,因此線程1對inc進行加1操做後inc的值爲11
  ,而後將11寫入工做內存,最後寫入主存。   那麼兩個線程分別進行了一次自增操做後,inc只增長了1.
  根源就在這裏,自增操做不是原子性操做,並且volatile也沒法保證對變量的任何操做都是原子性的。
  採用synchronized、lock、ActomicInteger能夠達到原子性
  在java 1.5的java.util.concurrent.atomic包下提供了一些原子操做類。atomic是利用CAS來實現原子性操做的(Compare And Swap)
  CAS其實是利用處理器提供的CMPXCHG指令實現的,而處理器執行CMPXCHG指令是一個原子性操做。

  另外:volatile 不能保證提供操做的原子性,但能夠利用其內存屏障(long類型得分兩次寫入,用valatile修飾後,只有當徹底寫入了,後面才能去使用),
  如讀 64 位數據類型,像 long 和 double 都不是原子的,但 volatile 類型的 double 和 long 就是原子的。

2.可見性

  對於可見性,Java提供了volatile關鍵字來保證可見性。

  當一個共享變量被volatile修飾時,它會保證修改的值會當即被更新到主存,當有其餘線程須要讀取時,它會去內存中讀取新值。

  而普通的共享變量不能保證可見性,由於普通共享變量被修改以後,何時被寫入主存是不肯定的,當其餘線程去讀取時,此時內存中可能仍是原來的舊值,所以沒法保證可見性。

  另外,經過synchronized和Lock也可以保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖而後執行同步代碼,而且在釋放鎖以前會將對變量的修改刷新到主存當中。所以能夠保證可見性。此外:final修飾的(一旦初始化完成,其餘線程也就可見)

eg:
public class VolatileTest extends Thread{

        private volatile boolean stop = false;
        public void stopMe(){
        stop=true;//volatile 寫操做
        }

        public void run(){
        int i=0;
        while(!stop){//volatile 讀操做,剛開始爲false,當前線程本地爲false,監聽到volatile 寫操做,而後去load主存
        i++;
                     }
        
        System.out.println("Stop thread:"+i);//1466907008
                                              //1319723236
        }

        public static void main(String args[]) throws InterruptedException{
            VolatileTest t=new VolatileTest();
        t.start();
        //Thread.sleep(1000);
        t.stopMe();
        //Thread.sleep(1000);
    
        }

}

可是用volatile修飾以後就變得不同了:

  第一:使用volatile關鍵字會強制將修改的值當即寫入主存;//通常的讀取仍是從工做內存讀取

  第二:使用volatile關鍵字的話,當線程t進行修改時,會致使main線程的工做內存中緩存變量stop的緩存行無效

  第三:因爲main線程的工做內存中緩存變量stop的緩存行無效,因此須要再次讀取變量stop的值時,這時會去主存讀取。

 
  

那麼在線程t修改stop值時(固然這裏包括2個操做,工做內存store+主內存write),會使得main線程的工做內存中緩存變量stop的緩存行無效,而後main線程讀取時,發現本身的緩存行無效(無效狀態),它會等待緩存行對應的主存地址被更新以後,而後去對應的主存讀取最新的值,那麼main線程讀取到的就是最新的正確的值。

 

聯想上文中提到的緩存一致性協議。最出名的就是Intel 的MESI協議,MESI協議保證了每一個緩存中使用的共享變量的副本是一致的。它核心的思想是:當CPU寫數據時,若是發現操做的變量是共享變量,即在其餘CPU中也存在該變量的副本,會發出信號通知其餘CPU將該變量的緩存行置爲無效狀態,所以當其餘CPU須要讀取這個變量時(觸發操做),發現本身緩存中緩存該變量的緩存行是無效的,那麼它就會從內存從新讀取。

 
3.有序性

volatile能保證有序性

在前面提到volatile關鍵字能禁止指令重排序,因此volatile能在必定程度上保證有序性。

 volatile關鍵字禁止指令重排序有兩層意思:

  1)當程序執行到volatile變量的讀操做或者寫操做時,在其前面的操做的更改確定所有已經進行,且結果已經對後面的操做可見;在其後面的操做確定尚未進行;

  即volatile是分水嶺。

  2)在進行指令優化時,不能將在對volatile變量訪問的語句放在其後面執行,也不能把volatile變量後面的語句放到其前面執行。

  可能上面說的比較繞,舉個簡單的例子:

//x、y爲非volatile變量
//flag爲volatile變量
 
x = 2;        //語句1
y = 0;        //語句2
flag = true;  //語句3
x = 4;         //語句4
y = -1;       //語句5

因爲flag變量爲volatile變量,那麼在進行指令重排序的過程的時候,不會將語句3放到語句一、語句2前面,也不會講語句3放到語句四、語句5後面。可是要注意語句1和語句2的順序、語句4和語句5的順序是不做任何保證的。

而且volatile關鍵字能保證,執行到語句3時,語句1和語句2一定是執行完畢了的,且語句1和語句2的執行結果對語句三、語句四、語句5是可見的。

那麼咱們回到前面舉的一個例子:

//線程1:
context = loadContext();   //語句1
inited = true;             //語句2
 
//線程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);
   前面舉這個例子的時候,提到有可能語句2會在語句1以前執行,那麼久可能致使context還沒被初始化,而線程2中就使用未初始化的context去進行操做,致使程序出錯。

  這裏若是用volatile關鍵字對inited變量進行修飾,就不會出現這種問題了,由於當執行到語句2時,一定能保證context已經初始化完畢。
Java內存模型具有一些先天的「有序性」,即不須要經過任何手段就可以獲得保證的有序性,這個一般也稱爲 happens-before 原則。若是兩個操做的執行次序沒法從happens-before原則推導出來,那麼就不能保證它們的有序性,虛擬機能夠隨意地對它們進行重排。
指令重排的基本原則(知足這些規則將不用重排)或者說happens-before規則:
1.程序順序原則,一個線程內保證語義的串行性。
2.volatile規則:volatile關鍵字變量的寫是先發生於讀的。
3.鎖規則:解鎖必然發生隨後加鎖以前。
4.傳遞性:A先於B,B先於C,那麼A必然先於C。
5.線程啓動規則:線程的start方法優先於該線程的其餘方法
6.線程中斷規則:線程的中斷(interrupt())先於線程被中斷後的代碼。
7.線程終結規則:線程的全部操做先於線程的終結(Thread.join())。
8.對象終結規則:對象的構造函數執行結束先於finalize()方法的調用。

volatile的原理和實現機制

  前面講述了源於volatile關鍵字的一些使用,下面咱們來探討一下volatile到底如何保證可見性和禁止指令重排序的。

  加入volatile關鍵字時,會多出一個lock前綴指令

  lock前綴指令實際上至關於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:

  1)它確保指令重排序時不會把其後面的指令排到內存屏障以前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操做已經所有完成;即分水嶺

  2)它會強制將對緩存的修改操做當即寫入主存;

  3)若是是寫操做,它會致使其餘CPU中對應的緩存行無效。


使用volatile關鍵字的場景:

synchronized關鍵字是防止多個線程同時執行一段代碼,那麼就會很影響程序執行效率,而volatile關鍵字在某些狀況下性能要優於synchronized,可是要注意volatile關鍵字是沒法替代synchronized關鍵字的,由於volatile關鍵字沒法保證操做的原子性

1.狀態標記量 volatile boolean flag = false;
 
while(!flag){
    doSomething();
}
 
public void setFlag() {
    flag = true;
}
 

volatile boolean inited = false;
//線程1:
context = loadContext();  
inited = true;            
 
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
 

2.double check class Singleton{
    private volatile static Singleton instance = null;
     
    private Singleton() {
         
    }
     
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();//個人感受是,雖然構造行爲是原子的,可是寫入的是當前線程的工做內存,會讓其餘線程instance==null成立,從而再次構造。
            }
        }
        return instance;
    }
}
   至於爲什麼須要這麼寫請參考:

  《Java 中的雙重檢查(Double-Check)》http://blog.csdn.net/dl88250/article/details/5439024

  和http://www.iteye.com/topic/652440

解釋運行:

  解釋執行以解釋方式運行字節碼 解釋執行的意思是:讀一句執行一句

編譯運行(JIT):

  將字節碼編譯成機器碼 直接執行機器碼 運行時編譯 編譯後性能有數量級的提高

相關文章
相關標籤/搜索