俗話說,本身寫的代碼,6個月後也是別人的代碼……複習!複習!複習!涉及到的知識點總結以下:java
爲何須要關注Java內存模型?c++
衆所周知,計算機某個運算的完成不只僅依靠cpu及其寄存器,還要和內存交互!cpu須要讀取內存中的運行數據,存儲運算結果到內存中……其中很天然的也是沒法避免的就涉及到了I/O操做,而常識告訴咱們,I/O操做和cpu的運算速度比起來,簡直沒得比!前者遠遠慢於後者(書上說相差幾個數量級!),前面JVM學習2也總結了這個情景,人們解決的方案是加緩存——cache(高速緩存),cache的讀寫速度儘量的接近cpu運算速度,來做爲內存和cpu之間的緩衝!舊的問題解決了,可是引起了新的問題!若是有多個cpu怎麼辦?程序員
現代操做系統都是多核心了,若是多個cpu和一塊內存進行交互,那麼每一個cpu都有本身的高速緩存塊……咋辦?也就是說,多個cpu的運算都訪問了同一塊內存塊的話,可能致使各個cpu的緩存數據不一致!if發生了上述情景,then以哪一個cpu的緩存爲主呢?爲了解決這個問題,人們想到,讓各個cpu在訪問緩存時都遵循某事先些規定的協議!由於無規矩不成方圓!如圖(如今能夠回答什麼是內存模型了):編程
什麼是內存模型?數組
通俗的說,就是在某些事先規定的訪問協議約束下,計算機處理器對內存或者高速緩存的訪問過程的一種抽象!這是物理機下的東西,其實對虛擬機來講(JVM),道理是同樣的!緩存
什麼是Java的內存模型(JMM)?安全
教科書這樣寫的:JVM規範說,Java程序在各個os平臺下必須實現一次編譯,處處運行的效果!故JVM規範定義了一個模型來屏蔽掉各種硬件和os之間內存訪問的差別(好比Java的併發程序必須在不一樣的os下運行效果是一致的)!這個模型就是Java的內存模型!簡稱JMM。多線程
讓我通俗的說:Java內存模型定義了把JVM中的變量存儲到內存和從內存中讀取出變量的訪問規則,這裏的變量不算Java棧內的局部變量,由於Java棧是線程私有的,不存在共享問題。細節上講,JVM中有一塊主內存(不是徹底對應物理機主內存的那個概念,這裏說的JVM的主內存是JVM的一部分,它主要對應Java堆中的對象實例及其相關信息的存儲部分)存儲了Java的全部變量。且Java的每個線程都有一個工做內存(對應Java棧),裏面存放了JVM主內存中變量的值的拷貝!且Java線程的工做內存和JVM的主內存獨立!如圖:架構
當數據從JVM的主內存複製一份拷貝到Java線程的工做內存存儲時,必須出現兩個動做:併發
反過來,當數據從線程的工做內存拷貝到JVM的主內存時,也出現兩個操做:
read,load,store,write的操做都是原子的,即執行期間不會被中斷!可是各個原子操做之間可能會發生中斷!對於普通變量,若是一個線程中那份JVM主內存變量值的拷貝更新了,並不能立刻反應在其餘變量中,由於Java的每一個線程都私有一個工做內存,裏面存儲了該條線程須要用到的JVM主內存中的變量拷貝!(好比實例的字段信息,類型的靜態變量,數組,對象……)如圖:
A,B兩條線程直接讀or寫的都是線程的工做內存!而A、B使用的數據從各自的工做內存傳遞到同一塊JVM主內存的這個過程是有時差的,或者說是有隔離的!通俗的說他們之間看不見!也就是以前說的一個線程中的變量被修改了,是沒法當即讓其餘線程看見的!若是須要在其餘線程中當即可見,須要使用 volatile 關鍵字。如今引出volatile關鍵字:
volatile 關鍵字是幹嗎的?舉例說明。
前面說了,各個線程之間的變量更新,若是想讓其餘線程當即可見,那麼須要使用它,故volatile字段是用於線程間通信的特殊字段。每次讀volatile字段都會看到其它線程寫入該字段的最新值!也就是說,一旦一個共享變量(成員、靜態)被volatile修飾,那麼就意味着:a線程修改了該變量的值,則這個新的值對其餘線程來講,是當即可見的!先看一個例子:
這段代碼會徹底運行正確麼?即必定會中斷麼?
//線程A boolean stop = false; while(!stop){ doSomething(); } //========= //線程B stop = true;
有些人在寫程序時,若是須要中斷線程,可能都會採用這種辦法。可是這樣作是有bug的!雖然這個可能性很小,可是隻要一旦bug發生,後果很嚴重!前面已經說了,Java的每一個線程在運行過程當中都有本身的工做內存,且Java的併發模型採用的是共享內存模型,Java線程之間的通訊老是隱式進行,整個通訊過程對程序員徹底透明,這也是爲何若是編寫多線程程序的Java程序員不理解隱式進行的線程之間通訊的工做機制,則極可能會遇到各類奇怪的併發問題的緣由。針對本題的A、B線程,若是他們之間通訊,畫成圖是這樣的:
那麼線程A和B須要通訊的時候,第一步A線程會將本地工做內存中的stop變量的值刷新到JVM主內存中,主內存的stop變量=false,第二步,線程B再去主內存中讀取stop的拷貝,臨時存儲在B,此時B中工做內存的stop也爲false了。當線程B更改了stop變量的值爲true以後,一樣也須要作相似線程A那樣的工做……可是此時此刻,偏偏B還沒來得及把更新以後的stop寫入主存當中(前面說了各個原子操做之間能夠中斷),就轉去作其餘事情了,那麼線程A因爲不知道線程B對stop變量的更改,所以還會一直循環下去。這就是死循環的潛在bug!
若是stop使用了volatile修飾,會使得:
這樣A獲得的就是最新的正確的stop值——true。程序完美的實現了中斷。不少人還認爲,volatile這麼好,它比鎖的性能好多了!其實這不是絕對的,很片面,只能說volatile比重量級的鎖(Java中線程是映射到操做系統的原生線程上的,若是要喚醒或者是阻塞一條線程須要操做系統的幫忙,這就須要從用戶態轉換到核心態,而狀態轉換須要至關長的時間……因此說syncronized關鍵字是java中比較重量級的操做)性能好,並且valatile萬萬不能代替鎖,由於它不是線程安全的,既volatile修飾符沒法保證對變量的任何操做都是原子的!(鑑於主要涉及了Java的併發編程,以後再開專題總結)。
什麼是原子性?
在Java中,對基本數據類型的變量的操做是原子性操做,即這些操做是不可被中斷的,要麼執行,要麼不執行。看例子:
1 int x = 10; //語句1 2 y = x; //語句2 3 x++; //語句3 4 x = x + 1; //語句4
這幾個語句哪一個是原子操做?
其實只有語句1是原子性操做,其餘三個語句都不是原子性操做。語句1是直接將數值10賦值給x,也就是說線程執行這個語句會直接將數值10寫入到工做內存中。線程執行語句2實際上包含2個操做,它先要去主內存讀取x的值,再將x的值寫入工做內存,雖然讀取x的值以及將x的值寫入工做內存這2個操做都是原子性操做,可是合起來就不是原子性操做了。一樣的,x++和 x = x+1包括3個操做:讀取x的值,進行加1操做,寫入新的值。因此上面4個語句只有語句1的操做具有原子性。也就是說,只有簡單的讀取、賦值(並且必須是將數字賦值給某個變量,變量之間的相互賦值不是原子操做)纔是原子操做。
不過這裏有一點須要注意:在32位平臺下,對64位數據的讀取和賦值是須要經過兩個操做來完成的,不能保證其原子性。可是好像在最新的JDK中,JVM已經保證對64位數據的讀取和賦值也是原子性操做了。從上面能夠看出,Java內存模型只保證了基本讀取和賦值是原子性操做,若是要實現更大範圍操做的原子性,能夠經過synchronized和Lock來實現。因爲synchronized和Lock可以保證任一時刻只有一個線程執行該代碼塊,那麼天然就不存在原子性問題了,從而保證了原子性。
什麼時候使用volatile關鍵字?
一般來講,使用volatile必須具有如下2個條件:
這些條件代表,能夠被寫入 volatile 變量的這些有效值獨立於任何程序的狀態,包括變量的當前狀態。個人理解就是上面的2個條件須要保證操做是原子性操做,才能保證使用volatile關鍵字的程序在併發時可以正確執行。好比boolean類型的標記變量。
前面只是大概總結了下Java的內存模式和volatile關鍵字,不是很深刻,留待後續併發專題補充。下面接着看幾個以前和以後會遇到的概念:
到底什麼是可見性?如何保證?
談談對指令重排的理解
a=1;
b=2;
先給a賦值,和先給b賦值,其實沒什麼區別,效果是同樣的,這樣的代碼就是可重排代碼,編譯器會針對上下文對指令作順序調整,哪一個順序好,就用哪一個,因此實際上兩句話怎麼個執行順序,是不必定的。
有可重排就天然會有不可重排,首先要知道Java內存模型具有一些先天的「有序性」,即不須要經過任何手段就可以保證有序性,這個一般也稱爲 happens-before 原則。若是兩個操做的執行次序沒法從happens-before原則推導出來,那麼它們就不能保證它們的有序性,虛擬機能夠隨意地對它們進行重排序。反之遵循了happen-before原則,JVM就沒法對指令進行重排序(看起來的)。這樣又引出了一個新問題:
什麼是先行發生原則happens-before?
下面就來具體介紹下happens-before(先行發生原則,這裏的先行和時間上先行是兩碼事;):
前4條規則是比較重要的,後4條規則都是常識。
好比像以下這樣的線程內的串行語義()是不可重排語句:
a = 1; b = a;// 寫一個變量以後,再讀這個變量。
a = 1; a = 2; // 寫一個變量以後,再寫這個變量。
a = b; b = 1; // 讀一個變量以後,再寫這個變量。
以上語句不可重排,單線程的程序看起來執行的順序是按照代碼順序執行的,這句話要正確理解:JVM實際上仍是可能會對程序代碼不存在數據依賴性的指令進行指令重排序,雖然進行重排序,可是最終執行的結果是與單線程的程序順序執行的結果一致的。所以,在單個線程中,程序執行看起來是有序執行的,這一點要注意理解。事實上,這個規則是用來保證程序在單線程中執行結果的正確性,但沒法保證程序在多線程中執行的正確性。對於多線程環境,編譯器不考慮多線程間的語義。看一個例子:
1 class OrderExample { 2 private int a = 0; 3 4 private boolean flag = false; 5 6 public void writer() { 7 a = 1; 8 flag = true; 9 } 10 11 public void reader() { 12 if (flag) { 13 int i = a + 1; 14 } 15 } 16 }
讓線程A首先執行writer()方法,接着讓線程B線程執行reader()方法,線程B若是看到了flag,那麼就可能會當即進入if語句,可是在int i=a+1處不必定能看到a已經被賦值爲1,由於在writer中,兩句話順序可能打亂!有可能對於B線程,它看A是無序的!編譯器沒法保證有序性。由於A徹底能夠先執行flag=true,再執行a=1,不影響結果!如圖:
也就是說多線程之間沒法保證指令的有序性!先行發生原則的程序次序有序性原則是針對單線程的。也就是說,若是是一個線程去前後執行這兩個方法,徹底是ok的!符合happens-before原則的第一條——程序次序有序性,故不存在指令重排問題。
如何解決呢?仍是套用先行發生原則,看第二條鎖定原則,咱們可使用同步鎖:
class OrderExample { private int a = 0; private boolean flag = false; public synchronized void writer() { a = 1; flag = true; } public synchronized void reader() { if (flag) { int i = a + 1; } } }
由於寫、讀都加鎖了,他們之間本質是串行的,即便線程A佔有寫鎖期間,JVM對寫作了指令重排也不要緊,由於此時鎖被A拿了,B線程沒法執行讀操做,直到A線程把寫操做執行完畢,釋放了該鎖,B線程才能拿到這同一個對象鎖,而此時,a確定是1,flag也必然是true了。此時必然是有序的。通俗的說,同步後,即便作了重排,由於互斥的緣故,reader 線程看writer線程也是順序執行的。
其餘語言(c和c++)也有內存模型麼?
大部分其餘的語言,像C和C++,都沒有被設計成直接支持多線程。這些語言對於發生在編譯器和處理器平臺架構的重排序行爲的保護機制會嚴重的依賴於程序中所使用的線程庫(例如pthreads),編譯器,以及代碼所運行的平臺所提供的保障。
最後補充下一個問題:Java的字節碼兩種運行方式——解釋執行和編譯執行