願我所遇之人,所歷之事,哪怕由於我有一點點變好,我就心滿意足了。git
提到volatile首先想到就是:程序員
到這裏你們感受本身對volatile理解了嗎?github
若是理解了,你們考慮這麼一個問題:ReentrantLock(或者其它基於AQS實現的鎖)是如何保證代碼段中變量(變量主要是指共享變量,存在競爭問題的變量)的可見性?面試
private static ReentrantLock reentrantLock = new ReentrantLock();
private static intcount = 0;
//...
// 多線程 run 以下代碼
reentrantLock.lock();
try
{
count++;
}
finally
{
reentrantLock.unlock();
}
//...
複製代碼
既然提到了可見性,那就先熟悉幾個概念:編程
JMM:Java Memory Model 即 Java 內存模型緩存
The Java Memory Model describes what behaviors are legal in multithreaded code, and how threads may interact through memory.bash
It describes the relationship between variables in a program and the low-level details of storing and retrieving them to and from memory or registers in a real computer system.微信
It does this in a way that can be implemented correctly using a wide variety of hardware and a wide variety of compiler optimizations.多線程
Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。此處的變量主要是指共享變量,存在競爭問題的變量。Java內存模型規定全部的變量都存儲在主內存中,而每條線程還有本身的工做內存,線程的工做內存中保存了該線程使用到的變量的主內存副本拷貝,線程對變量的全部操做(讀取、賦值等)都必須在工做內存中進行,而不能直接讀寫主內存中的變量(根據Java虛擬機規範的規定,volatile變量依然有共享內存的拷貝,可是因爲它特殊的操做順序性規定——從工做內存中讀寫數據前,必須先將主內存中的數據同步到工做內存中,全部看起來如同直接在主內存中讀寫訪問通常,所以這裏的描述對於volatile也不例外)。不一樣線程之間也沒法直接訪問對方工做內存中的變量,線程間變量值得傳遞均須要經過主內存來完成。併發
在執行程序時,爲了提升性能,編譯器和處理器經常會對指令作重排序。重排序分3種類型:
從Java源代碼到最終實際執行的指令序列,會分別經歷下面3種重排序:
對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(不是全部的編譯器重排序都要禁止)。對於處理器重排序,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定類型的內存屏障(Memory Barriers,Intel稱之爲Memory Fence)指令,經過內存屏障指令來禁止特定類型的處理器重排序。
JMM屬於語言級的內存模型,它確保在不一樣的編譯器和不一樣的處理器平臺之上,經過禁止特定類型的編譯器重排序和處理器重排序,爲程序員提供一致的內存可見性保證。
兩個操做之間具備happens-before關係,並不意味着前一個操做必需要在後一個操做以前執行!happens-before僅僅要求前一個操做(執行的結果)對後一個操做可見,且前一個操做按順序排在第二個操做以前(the first is visible to and ordered before the second)。
從JSR-133開始(即從JDK5開始),volatile變量的寫-讀能夠實現線程之間的通訊。
從內存語義的角度來講,volatile的寫-讀與鎖的釋放-獲取有相同的內存效果:
volatile僅僅保證對單個volatile變量的讀/寫具備原子性,而鎖的互斥執行的特性能夠確保對整個臨界區代碼的執行具備原子性。在功能上,鎖比volatile更強大;在可伸縮性和執行性能上,volatile更有優點。
volatile變量自身具備下列特性:
volatile寫和volatile讀的內存語義:
JMM針對編譯器制定的volatile重排序規則表
爲了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。對於編譯器來講,發現一個最優佈置來最小化插入屏障幾乎是不可能的。爲此,JMM採起保守策略。下面是基於保守策略的JMM內存屏障插入策略。
LoadLoad屏障:對於這樣的語句Load1; LoadLoad; Load2,在Load2及後續讀取操做要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。
StoreStore屏障:對於這樣的語句Store1; StoreStore; Store2,在Store2及後續寫入操做執行前,保證Store1的寫入操做對其它處理器可見。 LoadStore屏障:對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操做被刷出前,保證Load1要讀取的數據被讀取完畢。
StoreLoad屏障:對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續全部讀取操做執行前,保證Store1的寫入對全部處理器可見。它的開銷是四種屏障中最大的。在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種內存屏障的功能。 上述內存屏障插入策略很是保守,但它能夠保證在任意處理器平臺,任意的程序中都能獲得正確的volatile內存語義。
下面是保守策略下,volatile寫插入內存屏障後生成的指令序列示意圖.
圖中的StoreStore屏障能夠保證在volatile寫以前,其前面的全部普通寫操做已經對任意處理器可見了。這是由於StoreStore屏障將保障上面全部的普通寫在volatile寫以前刷新到主內存。
這裏比較有意思的是,volatile寫後面的StoreLoad屏障。此屏障的做用是避免volatile寫與後面可能有的volatile讀/寫操做重排序。由於編譯器經常沒法準確判斷在一個volatile寫的後面是否須要插入一個StoreLoad屏障(好比,一個volatile寫以後方法當即return)。爲了保證能正確實現volatile的內存語義,JMM在採起了保守策略:在每一個volatile寫的後面,或者在每一個volatile讀的前面插入一個StoreLoad屏障。從總體執行效率的角度考慮,JMM最終選擇了在每一個volatile寫的後面插入一個StoreLoad屏障。由於volatile寫-讀內存語義的常見使用模式是:一個寫線程寫volatile變量,多個讀線程讀同一個volatile變量。當讀線程的數量大大超過寫線程時,選擇在volatile寫以後插入StoreLoad屏障將帶來可觀的執行效率的提高。從這裏能夠看到JMM在實現上的一個特色:首先確保正確性,而後再去追求執行效率。
下面是在保守策略下,volatile讀插入內存屏障後生成的指令序列示意圖:
圖中的LoadLoad屏障用來禁止處理器把上面的volatile讀與下面的普通讀重排序。LoadStore屏障用來禁止處理器把上面的volatile讀與下面的普通寫重排序。上述volatile寫和volatile讀的內存屏障插入策略很是保守。在實際執行時,只要不改變volatile寫-讀的內存語義,編譯器能夠根據具體狀況省略沒必要要的屏障。
對於AQS須要瞭解這麼幾點:
AQS 詳解參見:面試必備:Java AQS 實現原理(圖文)分析
以公平鎖爲例,看看 ReentrantLock 獲取鎖 & 釋放鎖的關鍵代碼:
/**
* The synchronization state.
*/
private volatile int state;
/**
* Returns the current value of synchronization state.
* This operation has memory semantics of a {@code volatile} read.
* @return current state value
*/
protected final int getState() {
return state;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);// 釋放鎖的最後,寫volatile變量state
return free;
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();// 獲取鎖的開始,首先讀volatile變量state
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
複製代碼
公平鎖在釋放鎖的最後寫volatile變量state,在獲取鎖時首先讀這個volatile變量。根據volatile的happens-before規則,釋放鎖的線程在寫volatile變量以前可見的共享變量,在獲取鎖的線程讀取同一個volatile變量後將當即變得對獲取鎖的線程可見。從而保證了代碼段中變量(變量主要是指共享變量,存在競爭問題的變量)的可見性。
八、小結
若是咱們仔細分析concurrent包的源代碼實現,會發現一個通用化的實現模式。
前文咱們提到過,編譯器不會對volatile讀與volatile讀後面的任意內存操做重排序;編譯器不會對volatile寫與volatile寫前面的任意內存操做重排序。組合這兩個條件,意味着爲了同時實現volatile讀和volatile寫的內存語義,編譯器不能對CAS與CAS前面和後面的任意內存操做重排序。
本文參考:
一、《Java併發編程的藝術》 方騰飛 魏鵬 程曉明 著
我的微信公衆號: