參考https://www.cnblogs.com/dolphin0520/p/3920373.htmlhtml
1.java虛擬機啓動的命令是經過java +xxx(類名,這個類中要有main方法)或者javaw啓動的。java
2.執行命令後,系統第一步作的就是裝載配置,會在當前路徑中尋找jvm的config配置文件。算法
3.找到jvm的config配置文件以後會去定位jvm.dll這個文件。這個文件就是java虛擬機的主要實現。編程
4.當找到匹配當前版本的jvm.dll文件後,就會使用這個dll去初始化jvm虛擬機。得到相關的接口。以後找到main方法開始運行。緩存
上面這個過程的描述雖然比較簡單,可是jvm的啓動流程基本都已經涵蓋在裏面了。多線程
每一個線程擁有一個PC寄存器 在線程建立時 建立 指向下一條指令的地址 執行本地方法時,PC的值爲undefined併發
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 = 1;b = a; 寫一個變量以後,再讀這個位置。
寫後寫 a = 1;a = 2; 寫一個變量以後,再寫這個變量。
讀後寫 a = b;b = 1; 讀一個變量以後,再寫這個變量。
以上語句不可重排 編譯器不考慮多線程間的語義 可重排: a=1;b=2;
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不能保證原子性下面看一個例子: 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修飾的(一旦初始化完成,其餘線程也就可見)
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須要讀取這個變量時(觸發操做),發現本身緩存中緩存該變量的緩存行是無效的,那麼它就會從內存從新讀取。
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已經初始化完畢。
volatile的原理和實現機制
前面講述了源於volatile關鍵字的一些使用,下面咱們來探討一下volatile到底如何保證可見性和禁止指令重排序的。
加入volatile關鍵字時,會多出一個lock前綴指令
lock前綴指令實際上至關於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:
1)它確保指令重排序時不會把其後面的指令排到內存屏障以前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操做已經所有完成;即分水嶺
2)它會強制將對緩存的修改操做當即寫入主存;
3)若是是寫操做,它會致使其餘CPU中對應的緩存行無效。
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):
將字節碼編譯成機器碼 直接執行機器碼 運行時編譯 編譯後性能有數量級的提高