java內存模型知識導圖
一 併發問題及含義
併發編程存在原子性、可見性、有序性問題。
- 原子性即一系列操做要麼都執行,要麼都不執行。
- 可見性,一個線程對共享變量的修改,另外一個線程可能不會立刻看到。因爲多核CPU,每一個CPU核都有高速緩存,會緩存共享變量,某個線程對共享變量的修改會改變高速緩存中的值,但卻不會立刻寫入內存。另外一個線程讀到的是另外一個核緩存的共享變量的值,出現緩存不一致問題。
- 有序性,即程序執行的順序按照代碼的前後順序執行。編譯器和處理器會對指令進行重排,以優化指令執行性能,重排不會改變單線程執行結果,但在多線程中可能會引發各類各樣的問題。
二 內存模型
爲了保證共享內存的正確性(可見性、有序性、原子性),內存模型定義了共享內存系統中多線程程序讀寫操做行爲的規範。內存模型解決併發問題
主要採用兩種方式:限制處理器優化和使用內存屏障。
順序一致性內存模型是一種理論參考模型,提供了極強的內存可見性保證,具備兩大特性:
- 一個線程的全部操做按照程序的順序執行,而不能重排序。
- 全部線程只能看到單一的執行順序。每一個操做都必須原子執行且馬上對其它線程可見。
順序一致性內存模型禁止不少處理器和編譯器重排,影響執行性能,處理器內存模型和JMM對順序一致性內存模型進行放鬆,執行性能:處理器內存模型>JMM>順序一致性內存模型,易編程性:處理器內存模型<JMM<順序一致性內存模型。
三 java內存模型
Java內存模型(Java Memory Model ,JMM)是一種符合內存模型規範的,屏蔽了各類硬件和操做系統的訪問差別的,保證了Java程序在各類平臺下對內存的訪問都能保證效果一致的機制及規範。
Java內存模型規定了全部的變量都存儲在主內存中,每條線程還有本身的工做內存,線程的工做內存中保存了該線程中是用到的變量的主內存副本拷貝,線程對變量的全部操做都必須在工做內存中進行,而不能直接讀寫主內存。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程間變量的傳遞均須要本身的工做內存和主存之間進行數據同步進行。主內存和工做內存可類比成計算機內存模型中的主存和緩存的概念。
3.1 java內存模型解決併發問題方法
原子性,在java中,只有簡單的讀取、賦值(並且必須是將數字賦值給某個變量,變量之間的相互賦值不是原子操做)纔是原子操做。在32位平臺下,對64位數據的賦值是須要經過兩個操做來完成,不能保證其原子性。要實現更大範圍操做的原子性,能夠經過synchronized和Lock來實現。因爲synchronized和Lock保證任一時刻只有一個線程執行該代碼塊,從而保證了原子性。
可見性,Java提供了volatile關鍵字來保證可見性,當一個共享變量被volatile修飾時,它會保證修改的值會當即被更新到主存,當有其餘線程須要讀取時,它會去內存中讀取新值。經過synchronized和Lock也可以保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖而後執行同步代碼,而且在釋放鎖以前會將對變量的修改刷新到主存當中。所以能夠保證可見性。
JMM經過happens-before關係向程序員提供跨線程的內存可見性保證:
- 程序次序規則:一段代碼在單線程中執行的結果是有序的。注意是執行結果,由於虛擬機、處理器會對指令進行重排序(重排序後面會詳細介紹)。雖然重排序了,可是並不會影響程序的執行結果,因此程序最終執行的結果與順序執行的結果是一致的。故而這個規則只對單線程有效,在多線程環境下沒法保證正確性。
- 鎖定規則:這個規則比較好理解,不管是在單線程環境仍是多線程環境,一個鎖處於被鎖定狀態,那麼必須先執行unlock操做後面才能進行lock操做。
- volatile變量規則:這是一條比較重要的規則,它標誌着volatile保證了線程可見性。通俗點講就是若是一個線程先去寫一個volatile變量,而後一個線程去讀這個變量,那麼這個寫操做必定是happens-before讀操做的。
- 傳遞規則:提現了happens-before原則具備傳遞性,即A happens-before B , B happens-before C,那麼A happens-before C
- 線程啓動規則:假定線程A在執行過程當中,經過執行ThreadB.start()來啓動線程B,那麼線程A對共享變量的修改在接下來線程B開始執行後確保對線程B可見。
- 線程終結規則:假定線程A在執行的過程當中,經過制定ThreadB.join()等待線程B終止,那麼線程B在終止以前對共享變量的修改在線程A等待返回後可見。
有序性,可使用synchronized和volatile來保證多線程之間操做的有序性。實現方式有所區別:volatile關鍵字會禁止指令重排。synchronized關鍵字保證同一時刻只容許一條線程操做。
3.2 java併發原語
Java內存模型,除了定義了一套規範,還提供了一系列原語,封裝了底層實現後,供開發者直接使用。
3.2.1 volatile
內存語義:
當寫一個volatile變量時,JMM會把該線程對應的本地內存中的全部共享變量刷新到主內存。
當讀一個volatile變量,JMM會把該線程對應的本地內存置爲無效,線程接下來從主內存中讀取共享變量。
實現:
編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。
在每一個volatile寫操做前面插入一個StoreStore屏障。StoreStore屏障禁止上面的普通寫和volatile寫重排序,保障上面的普通寫在volatile寫以前刷新到主內存。
在每一個volatile寫操做後面插入一個StoreLoad屏障。避免volatile寫與後面可能有的volatile讀/寫重排序。
在每一個volatile讀操做的後面插入一個LoadLoad屏障。禁止下面的普通讀操做和上面的volatile讀操做重排序
在每一個volatile讀操做的後面插入一個LoadStore屏障。禁止下面的普通寫操做和上面的volatile讀操做重排序
3.2.2 synchronized
內存語義:
當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中.
當線程獲取鎖時,JMM會把該線程對應的本地內存置爲無效。從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量.
實現:
java對象頭組成:
- Mark Word
- 指向類的指針
- 數組長度(只有數組對象纔有)
Mark Word用於加鎖操做,結構以下:
圖3.1 java對象頭Mark Word
synchronized用的鎖是存在Java對象頭裏,任何java對象都存在一個鎖,JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步。代碼塊同步是使用monitorenter和monitorexit指令實現的,monitorenter指令是在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處。
監視器鎖(Monitor)本質依賴操做系統的Mutex Lock(互斥鎖)來實現,若是互斥量已經上鎖,調用線程會阻塞,阻塞或喚醒一條線程,都須要操做系統來幫忙完成,這就須要從用戶態轉換到核心態中,所以狀態轉換須要耗費不少的處理器時間。在jdk1.6中加入對鎖的優化措施,鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態。鎖能夠升級但不能降級。
偏向鎖:
當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,之後該線程在進入和退出同步塊時不須要進行CAS操做來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word裏是否存儲着指向當前線程的偏向鎖。引入偏向鎖是爲了在無多線程競爭的狀況下儘可能減小沒必要要的輕量級鎖執行路徑,由於輕量級鎖的獲取及釋放依賴屢次CAS原子指令,而偏向鎖只須要在置換ThreadID的時候依賴一次CAS原子指令(因爲一旦出現多線程競爭的狀況就必須撤銷偏向鎖,因此偏向鎖的撤銷操做的性能損耗必須小於節省下來的CAS原子指令的性能消耗)。
偏向鎖的獲取:
-
獲取對象的Mark Word
-
判斷對象是否處於可偏向狀態,是不是偏向鎖標誌位爲1,鎖標誌位爲01。
-
若是Mark Word中的線程ID爲空,用cas將此字段設爲當前線程,若成功則進入同步塊,不然進入步驟4;若線程ID爲當前線程,則直接進入同步塊,若爲其它線程則進入步驟4
-
若執行cas失敗,表示多個線程在競爭鎖,到達全局安全點後,得到偏向鎖的線程被掛起,撤銷偏向鎖,並升級爲輕量級,升級完成後被阻塞在安全點的線程繼續執行同步代碼塊。
偏向鎖的撤銷(只有當其它線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖):
-
偏向鎖的撤銷動做必須等待全局安全點;
-
暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態;
-
撤銷偏向鎖,恢復到無鎖(標誌位爲 01)或輕量級鎖(標誌位爲 00)的狀態
輕量級鎖:
輕量級鎖是爲了在線程近乎交替執行同步塊時提升性能。多個線程競爭鎖,若當前只有一個等待線程,則可經過自旋稍微等待一下,可能另外一個線程很快就會釋放鎖。 可是當自旋超過必定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖膨脹爲重量級鎖。
輕量級鎖獲取:
-
markOop mark = obj->mark()方法獲取對象的markOop數據mark;
-
mark->is_neutral()方法判斷mark是否爲無鎖狀態:mark的偏向鎖標誌位爲 0,鎖標誌位爲 01;
-
若是mark處於無鎖狀態,則進入步驟(4),不然執行步驟(6);
-
把mark保存到BasicLock對象的_displaced_header字段;
-
經過CAS嘗試將Mark Word更新爲指向BasicLock對象的指針,若是更新成功,表示競爭到鎖,則執行同步代碼,不然執行步驟(6);
-
若是當前mark處於加鎖狀態,且mark中的ptr指針指向當前線程的棧幀,則執行同步代碼,不然說明有多個線程競爭輕量級鎖,輕量級鎖須要膨脹升級爲重量級鎖
輕量級鎖撤銷:
-
取出在獲取輕量級鎖時保存在線程棧的中的Mark Word
-
經過CAS嘗試把保存的Mark Word替換到當前的Mark Word,若是CAS成功,說明成功的釋放了鎖,不然執行步驟(3)
-
若是CAS失敗,說明有其它線程在嘗試獲取該鎖,這時須要將該鎖升級爲重量級鎖,並釋放;
重量級鎖:
重量級鎖是經過對象內部的一個叫作監視器鎖(monitor)來實現的,監視器鎖本質又是依賴於底層的操做系統的Mutex Lock(互斥鎖)來實現的。而操做系統實現線程之間的切換須要從用戶態轉換到核心態,這個成本很是高。
其它鎖優化措施:鎖消除、鎖粗化、自旋鎖(忙循環,適用持有鎖的線程很快釋放鎖)、自適應的自旋鎖(自旋次數不固定,前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態決定)。
鎖膨脹過程:
-
mark->has_monitor()方法判斷當前是否爲重量級鎖,即Mark Word的鎖標識位爲 10,若是當前狀態爲重量級鎖,執行步驟(2),不然執行步驟(3);
-
mark->monitor()方法獲取指向ObjectMonitor的指針,並返回,說明膨脹過程已經完成;
-
若是當前鎖處於膨脹中,說明該鎖正在被其它線程執行膨脹操做,則當前線程就進行自旋等待鎖膨脹完成,這裏須要注意一點,雖然是自旋操做,但不會一直佔用cpu資源,每隔一段時間會經過os::NakedYield方法放棄cpu資源,或經過park方法掛起;若是其餘線程完成鎖的膨脹操做,則退出自旋並返回;
-
若是當前是輕量級鎖狀態,即鎖標識位爲 00,膨脹過程以下:
-
經過omAlloc方法,獲取一個可用的ObjectMonitor monitor,並重置monitor數據;
-
經過CAS嘗試將Mark Word設置爲markOopDesc:INFLATING,標識當前鎖正在膨脹中,若是CAS失敗,說明同一時刻其它線程已經將Mark Word設置爲markOopDesc:INFLATING,當前線程進行自旋等待膨脹完成;
-
若是CAS成功,設置monitor的各個字段:_header、_owner和_object等,並返回;
monitor競爭:
-
若是還未有線程獲取鎖,經過CAS嘗試把monitor的_owner字段設置爲當前線程;
-
若是設置以前的_owner指向當前線程,說明當前線程再次進入monitor,即重入鎖,執行_recursions ++ ,記錄重入的次數;
-
若是線程是第一次進入該monitor,設置_recursions爲1,_owner爲當前線程,該線程成功得到鎖並返回;
-
若是獲取鎖失敗,則等待鎖的釋放;
monitor等待:
-
monitor競爭失敗的線程,經過自旋等待鎖的釋放
-
當前線程被封裝成ObjectWaiter對象node,經過CAS把node節點push到ObjectMonitor中的cxq列表中
-
node節點push到_cxq列表以後,經過自旋嘗試獲取鎖,若是仍是沒有獲取到鎖,則經過park將當前線程掛起,等待被喚醒
monitor釋放:
根據不一樣的策略(由QMode指定),從ObjectMonitor中的cxq或EntryList中獲取頭節點,喚醒該節點封裝的線程,喚醒操做最終由unpark完成
3.2.3 final
寫final域禁止把final域的寫重排序到構造函數以外。對於引用類型:在構造函數內對final域引用對象的成員域的寫入,與在構造函數外將這個被構造對象的引用賦值給引用變量,這兩個操做不能重排序。防止對象構造完成,未被初始化的final域被訪問(要達到此目的,還需確保被構造對象不能在構造函數中「逸出」)
讀final域禁止初次讀一個對象的引用和隨後初次讀這個對象包含的final域之間的重排序。確保在讀一個對象的final域前,必定會先讀包含這個final域對象的引用,若是引用不爲空,引用對象的final域已經被初始化過。
實現:
JMM禁止編譯器把final域的寫重排序到構造函數以外。
編譯器在final域的寫以後,構造函數return以前,插入StoreStore屏障,禁止處理器把final域的寫重排序到構造函數以外。
編譯器會在讀final域前面插入StoreStore屏障。
參考文獻
Java併發編程:volatile關鍵字解析.
java內存模型(JMM)總結.
不得不瞭解的對象頭.
Java synchronized原理總結.
再有人問你Java內存模型是什麼,就把這篇文章發給他.
JVM內存結構 VS Java內存模型 VS Java對象模型.html