在Java中,虛擬機將運行時區域分紅6種,如圖:web
方法區:儲存符號引用、被JVM加載的類信息、靜態變量的地方。在Java8以後方法區被移除,使用元空間來存放類信息,常量池和其餘東西被移到堆中(其實在7的時候常量池和靜態變量就已經被移到堆中),再也不有永久代一說。刪除的緣由大體以下:安全
因爲類和方法的信息難以肯定,很差設定大小,太大則影響年老代,過小容易內存溢出。多線程
GC很差處理,回收效率低下,調優困難。併發
在上面的6種類型中,前三種是線程私有的,也就是說裏面存放的值其餘線程是看不到的,然後面三種(真正意義上講只有堆一種)是線程之間共享的,這裏面的變量對於各個線程都是可見的。以下圖所示,前三種存放在線程內存中,你們都是相互獨立的,而主內存能夠理解爲堆內存(實際上只是堆內存中的對象實例數據部分,其餘例如對象頭和對象的填充數據並不算入在內),爲線程之間共享:app
這裏的變量指的是能夠放在堆中的變量,其餘例如局部變量、方法參數這些並不算入在內。線程內存跟主內存變量之間的交互是很是重要的,Java虛擬機把這些交互規範爲如下8種操做,每一種都是原子性的(非volatile修飾的Double和Long除外)操做。ide
可能有人會不理解read和load、store和write的區別,以爲這兩對的操做相似,能夠把其當作一個是申請操做,另外一個是審覈經過(容許賦值)。例如:線程內存A向主內存提交了變動變量的申請(store操做),主內存經過以後修改變量的值(write操做)。以下圖:函數
參照《深刻理解Java虛擬機》this
對於普通的變量來講(非volatile修飾的變量),虛擬機要求read、load有相對順序便可,例如從主內存讀取i、j兩個變量,可能的操做是read i=>read j=>load j=> load i,並不必定是連續的。此外虛擬機還爲這8種操做定製了操做的規則:線程
對於關鍵字volatile,你們都知道其通常做爲併發的輕量級關鍵字,而且具備兩個重要的語義:3d
這兩個語義都是由於JMM對於volatile關鍵字修飾的變量會有特殊的規則:
- 在對變量執行use操做以前,其前一步操做必須爲對該變量的load操做;在對變量執行load操做以前,其後一步操做必須爲該變量的use操做。也就是說,使用volatile修飾的變量其read、load、use都是連續出現的,因此每次使用變量的時候都要從主內存讀取最新的變量值,替換私有內存的變量副本值(若是不一樣的話)。
- 在對變量執行assign操做以前,其後一步操做必須爲store;在對變量執行store以前,其前一步必須爲對相同變量的assign操做。也就是說,其對同一變量的assign、store、write操做都是連續出現的,因此每次對變量的改變都會立馬同步到主內存中。
- 在主內存中有變量a、b,動做A爲當前線程對變量a的use或者assign操做,動做B爲與動做A對應load或store操做,動做C爲與動做B對應的read或write操做;動做D爲當前線程對變量b的use或assign操做,動做E爲與D對應的load或store操做,動做F爲與動做E對應的read或write操做;若是動做A先於動做D,那麼動做C要先於動做F。也就是說,若是當前線程對變量a執行的use或assign操做在對變量buse或assign以前執行的話,那麼當前線程對變量a的read或write操做確定要在對變量b的read或write操做以前執行。
從上面volatile的特殊規則中,咱們能夠知道一、2條其實就是volatile內存可見性的語義,第三條就是禁止指令重排序的語義。另外還有其餘的一些特殊規則,例如對於非volatile修飾的double或者long這兩個64位的數據類型中,虛擬機容許對其當作兩次32位的操做來進行,也就是說能夠分解成非原子性的兩個操做,可是這種可能性出現的狀況也至關的小。由於Java內存模型雖然容許這樣子作,但卻「強烈建議」虛擬機選擇實現這兩種類型操做的原子性,因此平時不會出現讀到「半個變量」的狀況。
雖然volatile修飾的變量能夠強制刷新內存,可是其並不具有原子性,稍加思考就能夠理解,雖然其要求對變量的(read、load、use)、(assign、store、write)必須是連續出現,即以組的形式出現,可是這兩組操做仍是分開的。好比說,兩個線程同時完成了第一組操做(read、load、use),可是還沒進行第二組操做(assign、store、write),此時是沒錯的,而後兩個線程開始第二組操做,這樣最終其中一個線程的操做會被覆蓋掉,致使數據的不許確。以下面代碼:
public class TestForVolatile { public static volatile int i = 0; public static void main(String[] args) throws InterruptedException { // 建立四個線程,每一個線程對i執行必定次數的自增操做 new Thread(() -> { int k = 0; while (k++ < 10000) { i++; } System.err.println("線程" + Thread.currentThread().getName() + "執行完畢"); }).start(); new Thread(() -> { int k = 0; while (k++ < 10000) { i++; } System.err.println("線程" + Thread.currentThread().getName() + "執行完畢"); }).start(); new Thread(() -> { int k = 0; while (k++ < 10000) { i++; } System.err.println("線程" + Thread.currentThread().getName() + "執行完畢"); }).start(); new Thread(() -> { int k = 0; while (k++ < 10000) { i++; } System.err.println("線程" + Thread.currentThread().getName() + "執行完畢"); }).start(); // 睡眠必定時間確保四個線程所有執行完畢 Thread.sleep(1000); // 最終結果爲33555,沒有預期的4W System.out.println(i); } }
結果圖:
解釋一下:由於i++操做其實爲i = i + 1,假設在主內存i = 99的時候同時有兩個線程完成了第一組操做(read、load、use),也就是完成了等號後面變量i的讀取操做,這時候是沒問題的,而後進行運算,都得出i+1=100的結果,接着對變量i進行賦值操做,這就開始第二組操做(assign、store、write),是否是同時賦值的無所謂,這樣一來,兩個線程都會以i = 100把值寫到主內存中,也就是說,其中一個線程的操做結果會被覆蓋,至關於無效操做,這就致使上面程序最終結果的不許確。
若是要保證原子性的話可使用synchronize關鍵字,其能夠保證原子性和內存可見性(可是不具有有禁止指令重排序的語義,這也是爲何double-check的單例模式中,實例要用volatile修飾的緣由);固然你也可使用JUC包的原子類AtomicInteger之類的。
若是單靠volatile和synchronized來維持程序的有序性的話,那麼不免會變得有些繁瑣。然而大部分時候咱們並不須要這樣作,由於Java中有一個「先行發生原則」:若是操做A先行發生於操做B,那麼進行B操做以前A操做的變化都能被B操做觀察到,也就是說B能看到A對變量進行的修改。 這裏的前後指的是執行順序的前後,與時間無關。例如在下面僞代碼中:
// 在線程A執行,定爲A操做 i = 0; // 線程B執行,定義爲B操做 j = i; // 線程C執行,定義爲C操做 i = 1;
假設A操做先於B操做發生,暫時忽略C操做,那麼最終獲得的結果一定是i = j = 1;可是若是此時加入C操做,而且跟A、B操做沒有肯定先行發生關係,那麼最終的結果就變成了不肯定,由於C可能在B以前執行也可能在B以後執行,因此此時就會出現數據不許確的狀況。若是一開始沒有A操做先行於B操做這個前提的話,那麼就算沒有C操做,結果也是不肯定的。
固然,符合先行發生原則的並不必定按照這個規則來執行,只有在操做之間會有依賴的時候(即下一個操做用到上個操做的變量),此時的先行發生原則才必定適用。例如在下面的僞代碼中,雖然符合先行發生原則,可是也不保證能有序執行。
// 同一線程執行如下操做 // A操做 int i = 0; // B操做 int j = 1;
這裏徹底符合程序次序規則(先行發生原則的一種),可是兩個操做之間並無依賴,因此虛擬機徹底能夠對其進行重排序,使得B操做在A操做以前執行,固然這對程序的正確性並無影響。
那麼該如何判斷是否符合先行發生原則呢?就連前面的例子都是經過假設來得出先行發生的。莫慌,Java內存模型爲咱們提供一些規則,只要符合這些規則之一,那就符合先行發生原則。能夠類比爲先行發生原則爲接口,下面的規則則爲實現此接口的實現類。
線程啓動規則:一個線程的start()方法先行發生於該線程的每個動做,也就是說線程的start()方法要先於該線程的run()方法中的任何操做。以下面例子,我在線程A中改變了共享變量i的值,而後在啓動B線程,B線程中run方法是讀取並打印i的值,執行1W次,最終的結果讀取到的都爲1:
public static int i = 0; public static void main(String[] args) { for (int k = 0; k < 10000; k++) testThread(); } public static void testThread() { Thread threadB = new Thread(() -> { System.err.println("線程B中i的值爲:" + i); System.err.println("線程B執行結束"); }); new Thread(() -> { i = 1; // 在修改了共享變量i的值後,啓動線程B threadB.start(); System.err.println("線程A中執行完以後i的值爲:" + i); }).start(); }
結果圖:
線程終止規則:線程的全部操做先行於該線程的終止檢測,也就是先於join()方法執行。以下面代碼中,我在A線程對共享變量i執行100W的自增,再執行100W-1的自減,執行1000次左右,最終join的全部結果都必定是1。
public static int i = 0; public static void main(String[] args) throws InterruptedException { // 執行1000次 for (int k = 0; k < 1000; k++) { i = 0; testThread(); } } public static void testThread() throws InterruptedException { Thread threadA = new Thread(() -> { int k = 0; while (k++ < 100 * 100 * 100) { i++; } while (--k > 1) { i--; } System.err.println("線程A中執行完以後i的值爲:" + i); }); threadA.start(); // 加上下面這段代碼的話,join以前讀到的i可能爲0也可能大於0(不必定是1),緣由是變量i主內存的read和write操做沒有固定順序 // TimeUnit.NANOSECONDS.sleep(1); System.out.println("主線程中開啓線程A後i的值爲:" + i); // 線程A終止 threadA.join(); // join以後的結果必定爲1 System.err.println("Join以後i的值爲:" + i); }
結果圖:
這8種就是Java提供的不須要任何同步器的天然規則了,只要符合在8條之一,那麼就符合先行發生原則;反之,則否則。能夠經過下面的例子理解:
// 對象中有一個變量i private int i = 0; public int getI() { return i; } public void setI(int i) { this.i = i; } // 在線程A執行set操做A setI(1); // 在線程B執行相同對象的get操做B int j = getI();
咱們假設在時間上A操做先執行,而後再接着執行B操做,那麼B獲得的i是多少呢?
咱們將上面的規則一個個的往裏套,不一樣線程,程序次序規則OUT;沒有加鎖和volatile關鍵字,管程鎖定和volatile變量規則OUT;關於線程的三個規則和對象終止規則也不符合,OUT;最後一個更不用提,OUT;綜上,這個操做並不符合先行發生原則,因此這個操做是無法保證的,也就是說B獲得的變量i爲1爲0都有可能,便是線程不安全的。因此判斷線程是否安全的依據是先行發生原則,跟時間順序並無太大的關係。
像上面這種狀況要修正的話,使其符合其中一條規則便可,例如加上volatile關鍵字或者加鎖(同一把鎖)均可以解決這個問題。