今天週末,閒來無事,幹嗎呢?固然看書啊,總結啊!讀完書光回想是沒用的,必須有個本身的第一遍理解,第二遍理解.....,就好比簡簡單單的JMM說來輕鬆,網上博客雖多,圖文代碼加以解釋的甚少,並無給讀者一種層次感。因此我想寫這麼一篇博客,算是總結本身的第一遍理解,同時盡本身最大的可能讓你們理解的時候有一種層次感。數組
整篇博客都是參考《深刻理解Java虛擬機》加上本身讀了兩遍以後的理解完成的,創做不易,望轉載告之,謝謝!安全
先在記錄此篇博客以前,給一個大概的目錄結構方便讀者留意:多線程
一、Java內存模型介紹併發
二、Volatile關鍵字規則ide
三、double與long的非原子性高併發
第一次見到Java的內存模型,正如《深刻理解JVM》中那樣提到Cache與主存的關係,我也第一時間想起來了這個,因而便畫了以下的存儲系統的層次結構圖性能
Cache與主內存之間爲了能保持一致性,會不斷跟Cache進行交互,也就是地址映像,主要有直接映像、全相聯映像、組相聯映像,emmmm...打住,這不是正題,只是順便給本身個機會看《操做系統》就當作複習下,好了接下來是正題,先畫出JMM圖以下:優化
從圖中能夠看出要學好Java線程(高併發)是必需要知道JMM的,同時工做內存就比如Cache,與主內存之間進行交互,須要注意的的是這裏的工做內存與主內存並非咱們所知道的內存這個概念,也不僅是簡單的Java Heap與Java Stack那樣簡單的概念,爲了進一步知道工做內存與主內存是什麼,接下來先了解它們,此時你能夠先不用看圖,瞭解後再看更佳。spa
(1)工做內存:每條線程都有本身的內存,這就是工做內存,在工做內存中主要是保存使用到的變量在主內存的拷貝(即存放主內存中工做內存用到的變量拷貝);操作系統
(2)主內存:VM內存的一部分,是新增變量的地方以及每一個線程中全部變量來源之處,是能夠被共享的數據元素。
(3)內存模型中的變量:是指實例字段與靜態字段構成數組對象的元素,即能被共享的數據元素,而不是被線程私有的局部變量與方法參數等。全部的變量都會存儲在主內存(VM內存的一部分)中;
(4)每條線程對變量的操做都必須在工做內存中進行,而不能直接操做主內存;
(5)每條線程之間的工做內存是不能被共享的,不能相互訪問各自的變量,線程之間的變量「交流」只能經過主內存來實現;
(6)若是非要將JMM中的主內存與工做內存跟Java Heap、Java Stack、Method Area作比較(實則二者不是一個概念),那麼能夠認爲工做內存就是Java Stack(很好理解,這是由於Java Stack是線程私有的,線程之間不能共享),主內存就是Java Heap中實例數據(很好理解,Java Heap中對象的實例數據是能夠共享的)
這是理解多線程最重要的部分,多線程必然會涉及到內存之間的交互,Java的多線程之間的交互實則就是工做內存與主內存之間的交互,那麼它們之間確定要有相互交互的規定(即協議),主要分爲八種:
(1) Lock:做用於主內存的變量,將該變量標識爲某一條線程獨佔的資源,其餘線程不能佔用。
(2) Unlock:與Lock相反,做用於主內存變量,釋放被Lock的變量。
(3)Read:做用於主內存的變量,將該變量從主內從中取出,傳到線程的工做內存中。以後Load加載到工做內存的變量副本中。
(4) Load:將Read取到的變量放入工做內存的變量副本中。
(5) Use:將工做內存中變量傳遞給執行引擎,遵從VM指令的安排(被使用到時會有相關指令)
(6)Assign:接受執行引擎返回Use以後執行的結果值,將該值賦值給工做內存中對應的變量。
(7)Store:將功能內存中的值傳遞到主內存中,以後Write放入主內存的變量中。
(8) Write:將Store的值放入主內存的變量中。
好了,忽然一會兒要記住八種操做,頭也會並且可能還記不住,那麼結合圖總結下吧:
(1) 要把一個變量從主內存copy到工做內存,只須要Read->Load順序便可。
(其中Variable Duplicate變量拷貝是屬於工做內存Working Memory的,這裏主要是爲了能更好的展現,因此分離了,但願不要誤解!)
(2)若是把工做內存的變量同步回主內存,只須要Store->Write順序便可。
(其中Variable是屬於Main Memory的,這裏主要是爲了能更好的展現,因此分離了,但願不要誤解!)
(3) 若是VM用到這個變量(即有關的操做指令),則執行Use->Assign便可。
這裏就不畫圖了,簡單來講就是咱們在程序中用到變量,對變量初始化、更新等,也就是隻要在VM中有相關操做該變量的指令,就會從工做內存中被Use,以後Assign賦值會寫回公共內存,如i++,先拿到i,以後i+1,最後賦值i=i。
注意:這些操做之間並不要求必定要連續,只要保證先後順序便可,好比Read A, Read B, Load A, Load B便可,而不須要Read A, Load A, Read B, Load B
微觀上講咱們須要實現線程的一致性這個目標,而宏觀上就是如何確保在高併發下是安全的,其實主要是經過八種操做之間的規定,才能保證多線程下一致性:
(1) 不容許read和load,store和write中單一操做出現;
(2)不容許最近的賦值動做即assgin被丟棄(工做內存中變量改變了必須同步回主內存中);
(3) 不容許線程中變量沒有改變(即沒有assign操做),就把該變量數據同步回主內存(不接受毫無理由的同步回主內存);
(4) 一個變量的產生只能在主內存中,不容許工做內存使用一個未被初始化(即未被assgin賦值或load加載)的變量,即一個新的變量產生必須在主內存中,再read->load到工做內存變量副本中,以後進行中Use->Assign賦值,最後纔有可能stroe->write同步回主存,換句話說就是對一個變量進行use/store以前必須先進行assign/load操做。
(5)Lock與Unlock操做是成對出現的,一個變量只能被一個lock操做,一個線程能夠屢次lock操做。
(6) 一個線程的lock與unlock操做要對應,不容許線程A的unlock線程B的變量。同理,若是沒有lock,那麼不容許unlock。
(7) 一個變量執行lock操做,會將工做內存中對應的變量清空,在執行引擎獲取這個變量以前,必須load/assgin初始化這個變量,這是由於執行引擎要獲取的變量必須是最新的值,在lock-unlock過程當中該變量可能發生改變,因此必須從新初始化保證得到最新的值。
實際上,咱們在程序中操做的變量是工做內存的變量副本,那麼每次變量被改變(Use->Assign)後,都會同步回(Store->Write)主內存中,保持了變量的一致性,可是這只是單線程的狀況下,那麼在多線程狀況下呢?好比線程A已經改變了變量的值,還沒來的及同步回主內存,線程B就已經從主內存中將舊的變量值Read->Load到工做內存。這就形成了被線程A修改後的變量值對線程B不可見的問題,致使變量不一致。最輕量的能解決此問題就是利用好Volatile關鍵字,那麼Volatile是如何實現的呢?
簡單來講被Volatile關鍵字的變量一旦被改變後就會當即同步回內存中,保證其餘線程能得到最新的當前變量值,而廣泛變量不會當即同步回內存(事實上何時同步回內存是不肯定的),因此致使不可見性。
(1)保證此變量對全部線程的可見性:
① 線程的可見性並非誤認爲「Volatile對全部線程的當即可見,也就是對某個變量寫操做立馬能反映到全部線程中,所以在高併發的狀況下是安全的」,「Volatile在高併發下是安全的」這個最後的結論是不成立的。
② Java中相關的操做並非原子操做,好比i++,實際上是分爲兩步(可使用Javap反編譯查看指令代碼)的:先i+1,以後i=i+1。因此Volatile在高併發狀況下並非安全的。
1 /** 2 * 演示使用Volatile在高併發下狀態的不安全性: 3 * @author Jian 4 * 5 */ 6 public class VolatileDemo { 7 private static final int THREAD_NUM = 10;//線程數目 8 private static final long AWAIT_TIME = 5*1000;//等待時間 9 private volatile static int counter = 0; 10 11 public static void increase() { counter++; } 12 13 public static void main(String[] args) throws InterruptedException { 14 ExecutorService exe = Executors.newFixedThreadPool(THREAD_NUM); 15 for (int i = 0; i < THREAD_NUM; i++) { 16 exe.execute(new Runnable() { 17 @Override 18 public void run() { 19 for (int j = 0; j < 1000; j++) { 20 increase(); 21 } 22 } 23 }); 24 } 25 //檢測ExecutorService線程池任務結束而且是否關閉:通常結合shutdown與awaitTermination共同使用 26 //shutdown中止接收新的任務而且等待已經提交的任務 27 exe.shutdown(); 28 //awaitTermination等待超時設置,監控ExecutorService是否關閉 29 while (!exe.awaitTermination(AWAIT_TIME, TimeUnit.SECONDS)) { 30 System.out.println("線程池沒有關閉"); 31 } 32 System.out.println(counter); 33 } 34 }
按道理說最後變量i的結果應該是10*1000=10000,可是運行後你會發現輸出結果都是小於10000且各不相同的值,形成這樣的結果實則不是Volatile的鍋,而是Java的非原子性,只是但願咱們在關注並使用Volatile關鍵字的時候須要知道在高併發下不必定是安全的。
(2)使用Volatile能夠禁止指令重排序優化:
也就是通常普通變量(未被Volatile修飾)只能保證最後的變量結果是對的,可是不會保證變量涉及到的程序代碼中順序與底層執行指令順序是一致。須要注意的是重排序是一種編譯過程當中的一種優化手段。
下列只能用僞代碼的形式舉例,由於指令重排序涉及到反編譯指令碼等(我並不瞭解,實際上一點也不)
1 public class VolatileDemo2 { 2 //是否已經完成初始化標誌 3 private /*volatile*/ static boolean initialized = false; 4 private static int taskA = 0; 5 public static void main(String[] args) throws InterruptedException { 6 ExecutorService exe = Executors.newFixedThreadPool(2); 7 //線程A 8 exe.execute(new Runnable() { 9 @Override 10 public void run() { 11 //A線程的任務是加1,完成初始化 12 taskA++; 13 //initialized初始化完成,賦值爲true,必須是先執行+1操做,才能賦值true 14 //可是因爲重排序這裏可能先於taskA++執行,致使讀取到的結果可能爲0。 15 initialized = true; 16 } 17 }); 18 exe.execute(new Runnable() { 19 @Override 20 public void run() { 21 //線程B的任務是等待線程A初始化完成後,再讀取taskA的值 22 while(!initialized) { 23 try { 24 System.out.println("線程A還未初始化"); 25 Thread.sleep(1000); 26 } catch (InterruptedException e) { 27 e.printStackTrace(); 28 } 29 } 30 System.out.println(taskA); 31 } 32 }); 33 exe.shutdown(); 34 while (!exe.awaitTermination(5*1000, TimeUnit.SECONDS)) { 35 System.out.println("線程池沒有關閉"); 36 } 37 } 38 }
須要主要的就是下面的代碼,雖然線程A中是保證了有序執行,再標誌初始化完成,可是在指令中多是先賦initialized爲true,而後線程B這時候「搶先一步」先讀initialized,那麼變量taskA的值就可能爲0(實際業務中可能會是致命錯誤!)
taskA++; initialized = true;
若是不使用volatile關鍵字,那麼只有當明確賦值了initialized的方法被調用,接下來的任務才能不會出錯(只要結果是true就行,不用管指令順序):
boolean volatile initialized; public void setInitialized(){ initialized = true; } public otherWorks(){ //初始化完成方法被明確調用,強制initialized結果爲true,不用管指令順序 setInitialized(); while(!initialized){ //other thread's tasks } }
(3)Volatile與Synchronized性能對比:通常狀況下Volatile的同步機制要優於Synchronized(可是VM對Synchronized作了不少優化,因此其實也是說不許的),可是Volatile好就好在讀取變量跟普通變量的讀取幾乎沒啥差異,可是寫操做會慢一點(這是由於會在代碼中加入內存屏障,保證指令不會亂序)
(1)double與long的非原子性:在JMM中規定long與double這樣的64位而且沒有被volatile修飾數據能夠劃分爲兩部分32位來進行操做,即VM容許對64位的數據類型的load、store、read、write不保證其原子性。由於非原子性的存在,按理論上來講某個線程在極小的機率下可能會存在讀到「半個變量」的狀況。
(2)雖然因爲long與double非原子性存在,可是VM對其的操做是具備原子性的,即對操做原子性,對數據非原子性。因此long與double不須要被要求加volatile關鍵字。