摘要:前情提要深入理解內(nèi)存模型四鎖的釋放獲取建立的關(guān)系鎖是并發(fā)編程中最重要的同步機(jī)制。鎖內(nèi)存語義的實(shí)現(xiàn)本文將借助的源代碼,來分析鎖內(nèi)存語義的具體實(shí)現(xiàn)機(jī)制。請(qǐng)看下篇深入理解內(nèi)存模型六
前情提要 深入理解Java內(nèi)存模型(四)—— volatile
鎖的釋放-獲取建立的happens before 關(guān)系鎖是java并發(fā)編程中最重要的同步機(jī)制。鎖除了讓臨界區(qū)互斥執(zhí)行外,還可以讓釋放鎖的線程向獲取同一個(gè)鎖的線程發(fā)送消息。下面是鎖釋放-獲取的示例代碼:
class MonitorExample { int a = 0; public synchronized void writer() { //1 a++; //2 } //3 public synchronized void reader() { //4 int i = a; //5 …… } //6 }
假設(shè)線程A執(zhí)行writer()方法,隨后線程B執(zhí)行reader()方法。根據(jù)happens
before規(guī)則,這個(gè)過程包含的happens before 關(guān)系可以分為兩類:
根據(jù)程序次序規(guī)則,1 happens before 2, 2 happens before 3; 4 happens before 5, 5 happens before 6。
根據(jù)監(jiān)視器鎖規(guī)則,3 happens before 4。
根據(jù)happens before 的傳遞性,2 happens before 5。
上述happens before 關(guān)系的圖形化表現(xiàn)形式如下:
在上圖中,每一個(gè)箭頭鏈接的兩個(gè)節(jié)點(diǎn),代表了一個(gè)happens before 關(guān)系。黑色箭頭表示程序順序規(guī)則;橙色箭頭表示監(jiān)視器鎖規(guī)則;藍(lán)色箭頭表示組合這些規(guī)則后提供的happens before保證。
上圖表示在線程A釋放了鎖之后,隨后線程B獲取同一個(gè)鎖。在上圖中,2 happens before 5。因此,線程A在釋放鎖之前所有可見的共享變量,在線程B獲取同一個(gè)鎖之后,將立刻變得對(duì)B線程可見。
鎖釋放和獲取的內(nèi)存語義當(dāng)線程釋放鎖時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。以上面的MonitorExample程序?yàn)槔?,A線程釋放鎖后,共享數(shù)據(jù)的狀態(tài)示意圖如下:
當(dāng)線程獲取鎖時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無效。從而使得被監(jiān)視器保護(hù)的臨界區(qū)代碼必須要從主內(nèi)存中去讀取共享變量。下面是鎖獲取的狀態(tài)示意圖:
對(duì)比鎖釋放-獲取的內(nèi)存語義與volatile寫-讀的內(nèi)存語義,可以看出:鎖釋放與volatile寫有相同的內(nèi)存語義;鎖獲取與volatile讀有相同的內(nèi)存語義。
下面對(duì)鎖釋放和鎖獲取的內(nèi)存語義做個(gè)總結(jié):
線程A釋放一個(gè)鎖,實(shí)質(zhì)上是線程A向接下來將要獲取這個(gè)鎖的某個(gè)線程發(fā)出了(線程A對(duì)共享變量所做修改的)消息。
線程B獲取一個(gè)鎖,實(shí)質(zhì)上是線程B接收了之前某個(gè)線程發(fā)出的(在釋放這個(gè)鎖之前對(duì)共享變量所做修改的)消息。
線程A釋放鎖,隨后線程B獲取這個(gè)鎖,這個(gè)過程實(shí)質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息。
鎖內(nèi)存語義的實(shí)現(xiàn)本文將借助ReentrantLock的源代碼,來分析鎖內(nèi)存語義的具體實(shí)現(xiàn)機(jī)制。
請(qǐng)看下面的示例代碼:
class ReentrantLockExample { int a = 0; ReentrantLock lock = new ReentrantLock(); public void writer() { lock.lock(); //獲取鎖 try { a++; } finally { lock.unlock(); //釋放鎖 } } public void reader () { lock.lock(); //獲取鎖 try { int i = a; …… } finally { lock.unlock(); //釋放鎖 } } }
在ReentrantLock中,調(diào)用lock()方法獲取鎖;調(diào)用unlock()方法釋放鎖。
ReentrantLock的實(shí)現(xiàn)依賴于java同步器框架AbstractQueuedSynchronizer(本文簡(jiǎn)稱之為AQS)。AQS使用一個(gè)整型的volatile變量(命名為state)來維護(hù)同步狀態(tài),馬上我們會(huì)看到,這個(gè)volatile變量是ReentrantLock內(nèi)存語義實(shí)現(xiàn)的關(guān)鍵。 下面是ReentrantLock的類圖(僅畫出與本文相關(guān)的部分):
ReentrantLock分為公平鎖和非公平鎖,我們首先分析公平鎖。
使用公平鎖時(shí),加鎖方法lock()的方法調(diào)用軌跡如下:
ReentrantLock : lock()
FairSync : lock()
AbstractQueuedSynchronizer : acquire(int arg)
ReentrantLock : tryAcquire(int acquires)
在第4步真正開始加鎖,下面是該方法的源代碼:
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //獲取鎖的開始,首先讀volatile變量state if (c == 0) { if (isFirst(current) && 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。
在使用公平鎖時(shí),解鎖方法unlock()的方法調(diào)用軌跡如下:
ReentrantLock : unlock()
AbstractQueuedSynchronizer : release(int arg)
Sync : tryRelease(int releases)
在第3步真正開始釋放鎖,下面是該方法的源代碼:
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; }
從上面的源代碼我們可以看出,在釋放鎖的最后寫volatile變量state。
公平鎖在釋放鎖的最后寫volatile變量state;在獲取鎖時(shí)首先讀這個(gè)volatile變量。根據(jù)volatile的happens-before規(guī)則,釋放鎖的線程在寫volatile變量之前可見的共享變量,在獲取鎖的線程讀取同一個(gè)volatile變量后將立即變的對(duì)獲取鎖的線程可見。
現(xiàn)在我們分析非公平鎖的內(nèi)存語義的實(shí)現(xiàn)。
非公平鎖的釋放和公平鎖完全一樣,所以這里僅僅分析非公平鎖的獲取。
使用非公平鎖時(shí),加鎖方法lock()的方法調(diào)用軌跡如下:
ReentrantLock : lock()
NonfairSync : lock()
AbstractQueuedSynchronizer : compareAndSetState(int expect, int
update)
在第3步真正開始加鎖,下面是該方法的源代碼:
protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
該方法以原子操作的方式更新state變量,本文把java的compareAndSet()方法調(diào)用簡(jiǎn)稱為CAS。JDK文檔對(duì)該方法的說明如下:如果當(dāng)前狀態(tài)值等于預(yù)期值,則以原子方式將同步狀態(tài)設(shè)置為給定的更新值。此操作具有 volatile 讀和寫的內(nèi)存語義。
這里我們分別從編譯器和處理器的角度來分析,CAS如何同時(shí)具有volatile讀和volatile寫的內(nèi)存語義。
前文我們提到過,編譯器不會(huì)對(duì)volatile讀與volatile讀后面的任意內(nèi)存操作重排序;編譯器不會(huì)對(duì)volatile寫與volatile寫前面的任意內(nèi)存操作重排序。組合這兩個(gè)條件,意味著為了同時(shí)實(shí)現(xiàn)volatile讀和volatile寫的內(nèi)存語義,編譯器不能對(duì)CAS與CAS前面和后面的任意內(nèi)存操作重排序。
下面我們來分析在常見的intel x86處理器中,CAS是如何同時(shí)具有volatile讀和volatile寫的內(nèi)存語義的。
下面是sun.misc.Unsafe類的compareAndSwapInt()方法的源代碼:
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
可以看到這是個(gè)本地方法調(diào)用。這個(gè)本地方法在openjdk中依次調(diào)用的c++代碼為:unsafe.cpp,atomic.cpp和atomic*windows*x86.inline.hpp。這個(gè)本地方法的最終實(shí)現(xiàn)在openjdk的如下位置:openjdk-7-fcs-src-b147-27*jun*2011openjdkhotspotsrcos*cpuwindows*x86vmatomic*windows*x86.inline.hpp(對(duì)應(yīng)于windows操作系統(tǒng),X86處理器)。下面是對(duì)應(yīng)于intel x86處理器的源代碼的片段:
// Adding a lock prefix to an instruction on MP machine // VC++ doesn"t like the lock prefix to be on a single line // so we can"t insert a label after the lock prefix. // By emitting a lock prefix, we can define a label after it. #define LOCK_IF_MP(mp) __asm cmp mp, 0 __asm je L0 __asm _emit 0xF0 __asm L0: inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { // alternative for InterlockedCompareExchange int mp = os::is_MP(); __asm { mov edx, dest mov ecx, exchange_value mov eax, compare_value LOCK_IF_MP(mp) cmpxchg dword ptr [edx], ecx } }
如上面源代碼所示,程序會(huì)根據(jù)當(dāng)前處理器的類型來決定是否為cmpxchg指令添加lock前綴。如果程序是在多處理器上運(yùn)行,就為cmpxchg指令加上lock前綴(lock cmpxchg)。反之,如果程序是在單處理器上運(yùn)行,就省略lock前綴(單處理器自身會(huì)維護(hù)單處理器內(nèi)的順序一致性,不需要lock前綴提供的內(nèi)存屏障效果)。
intel的手冊(cè)對(duì)lock前綴的說明如下:
確保對(duì)內(nèi)存的讀-改-寫操作原子執(zhí)行。在Pentium及Pentium之前的處理器中,帶有l(wèi)ock前綴的指令在執(zhí)行期間會(huì)鎖住總線,使得其他處理器暫時(shí)無法通過總線訪問內(nèi)存。很顯然,這會(huì)帶來昂貴的開銷。從Pentium 4,Intel Xeon及P6處理器開始,intel在原有總線鎖的基礎(chǔ)上做了一個(gè)很有意義的優(yōu)化:如果要訪問的內(nèi)存區(qū)域(area of memory)在lock前綴指令執(zhí)行期間已經(jīng)在處理器內(nèi)部的緩存中被鎖定(即包含該內(nèi)存區(qū)域的緩存行當(dāng)前處于獨(dú)占或以修改狀態(tài)),并且該內(nèi)存區(qū)域被完全包含在單個(gè)緩存行(cache line)中,那么處理器將直接執(zhí)行該指令。由于在指令執(zhí)行期間該緩存行會(huì)一直被鎖定,其它處理器無法讀/寫該指令要訪問的內(nèi)存區(qū)域,因此能保證指令執(zhí)行的原子性。這個(gè)操作過程叫做緩存鎖定(cache locking),緩存鎖定將大大降低lock前綴指令的執(zhí)行開銷,但是當(dāng)多處理器之間的競(jìng)爭(zhēng)程度很高或者指令訪問的內(nèi)存地址未對(duì)齊時(shí),仍然會(huì)鎖住總線。
禁止該指令與之前和之后的讀和寫指令重排序。
把寫緩沖區(qū)中的所有數(shù)據(jù)刷新到內(nèi)存中。
上面的第2點(diǎn)和第3點(diǎn)所具有的內(nèi)存屏障效果,足以同時(shí)實(shí)現(xiàn)volatile讀和volatile寫的內(nèi)存語義。
經(jīng)過上面的這些分析,現(xiàn)在我們終于能明白為什么JDK文檔說CAS同時(shí)具有volatile讀和volatile寫的內(nèi)存語義了。
現(xiàn)在對(duì)公平鎖和非公平鎖的內(nèi)存語義做個(gè)總結(jié):
公平鎖和非公平鎖釋放時(shí),最后都要寫一個(gè)volatile變量state。
公平鎖獲取時(shí),首先會(huì)去讀這個(gè)volatile變量。
非公平鎖獲取時(shí),首先會(huì)用CAS更新這個(gè)volatile變量,這個(gè)操作同時(shí)具有volatile讀和volatile寫的內(nèi)存語義。
從本文對(duì)ReentrantLock的分析可以看出,鎖釋放-獲取的內(nèi)存語義的實(shí)現(xiàn)至少有下面兩種方式:
利用volatile變量的寫-讀所具有的內(nèi)存語義。
利用CAS所附帶的volatile讀和volatile寫的內(nèi)存語義。
concurrent包的實(shí)現(xiàn)由于java的CAS同時(shí)具有 volatile 讀和volatile寫的內(nèi)存語義,因此Java線程之間的通信現(xiàn)在有了下面四種方式:
A線程寫volatile變量,隨后B線程讀這個(gè)volatile變量。
A線程寫volatile變量,隨后B線程用CAS更新這個(gè)volatile變量。
A線程用CAS更新一個(gè)volatile變量,隨后B線程用CAS更新這個(gè)volatile變量。
A線程用CAS更新一個(gè)volatile變量,隨后B線程讀這個(gè)volatile變量。
Java的CAS會(huì)使用現(xiàn)代處理器上提供的高效機(jī)器級(jí)別原子指令,這些原子指令以原子方式對(duì)內(nèi)存執(zhí)行讀-改-寫操作,這是在多處理器中實(shí)現(xiàn)同步的關(guān)鍵(從本質(zhì)上來說,能夠支持原子性讀-改-寫指令的計(jì)算機(jī)器,是順序計(jì)算圖靈機(jī)的異步等價(jià)機(jī)器,因此任何現(xiàn)代的多處理器都會(huì)去支持某種能對(duì)內(nèi)存執(zhí)行原子性讀-改-寫操作的原子指令)。同時(shí),volatile變量的讀/寫和CAS可以實(shí)現(xiàn)線程之間的通信。把這些特性整合在一起,就形成了整個(gè)concurrent包得以實(shí)現(xiàn)的基石。如果我們仔細(xì)分析concurrent包的源代碼實(shí)現(xiàn),會(huì)發(fā)現(xiàn)一個(gè)通用化的實(shí)現(xiàn)模式:
首先,聲明共享變量為volatile;
然后,使用CAS的原子條件更新來實(shí)現(xiàn)線程之間的同步;
同時(shí),配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內(nèi)存語義來實(shí)現(xiàn)線程之間的通信。
AQS,非阻塞數(shù)據(jù)結(jié)構(gòu)和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包中的基礎(chǔ)類都是使用這種模式來實(shí)現(xiàn)的,而concurrent包中的高層類又是依賴于這些基礎(chǔ)類來實(shí)現(xiàn)的。從整體來看,concurrent包的實(shí)現(xiàn)示意圖如下:
參考文獻(xiàn)?Concurrent Programming in Java: Design Principles and Pattern
?JSR 133 (Java Memory Model) FAQ
?JSR-133: Java Memory Model and Thread Specification
?Java Concurrency in Practice
?Java? Platform, Standard Edition 6 API Specification
?The JSR-133 Cookbook for Compiler Writers
?Intel? 64 and IA-32 ArchitecturesvSoftware Developer’s Manual Volume 3A: System Programming Guide, Part 1
?The Art of Multiprocessor Programming
關(guān)于作者程曉明,Java軟件工程師,國家認(rèn)證的系統(tǒng)分析師、信息項(xiàng)目管理師。專注于并發(fā)編程,個(gè)人郵箱:asst2003@163.com。
請(qǐng)看下篇 深入理解Java內(nèi)存模型(六)——final
via ifeve
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/66150.html
摘要:前情提要深入理解內(nèi)存模型三順序一致性的特性當(dāng)我們聲明共享變量為后,對(duì)這個(gè)變量的讀寫將會(huì)很特別。當(dāng)讀線程的數(shù)量大大超過寫線程時(shí),選擇在寫之后插入屏障將帶來可觀的執(zhí)行效率的提升。 前情提要 深入理解Java內(nèi)存模型(三)——順序一致性 volatile的特性 當(dāng)我們聲明共享變量為volatile后,對(duì)這個(gè)變量的讀/寫將會(huì)很特別。理解volatile特性的一個(gè)好方法是:把對(duì)volatil...
摘要:本文從內(nèi)存模型角度,探討的實(shí)現(xiàn)原理。通過共享內(nèi)存或者消息通知這兩種方法,可以實(shí)現(xiàn)通信或同步?;诠蚕韮?nèi)存的線程通信是隱式的,線程同步是顯式的而基于消息通知的線程通信是顯式的,線程同步是隱式的。鎖規(guī)則鎖的解鎖,于于鎖的獲取或加鎖。 一、前言 在java多線程編程中,volatile可以用來定義輕量級(jí)的共享變量,它比synchronized的使用成本更低,因?yàn)樗粫?huì)引起線程上下文的切換和調(diào)...
摘要:的方法,的默認(rèn)實(shí)現(xiàn)會(huì)判斷是否是類型注意自動(dòng)拆箱,自動(dòng)裝箱問題。適應(yīng)自旋鎖鎖競(jìng)爭(zhēng)是下的,會(huì)經(jīng)過用戶態(tài)到內(nèi)核態(tài)的切換,是比較花時(shí)間的。在中引入了自適應(yīng)的自旋鎖,說明自旋的時(shí)間不固定,要不要自旋變得越來越聰明。 前言 只有光頭才能變強(qiáng) 之前在刷博客的時(shí)候,發(fā)現(xiàn)一些寫得比較好的博客都會(huì)默默收藏起來。最近在查閱補(bǔ)漏,有的知識(shí)點(diǎn)比較重要的,但是在之前的博客中還沒有寫到,于是趁著閑整理一下。 文本的...
摘要:我的是忙碌的一年,從年初備戰(zhàn)實(shí)習(xí)春招,年三十都在死磕源碼,三月份經(jīng)歷了阿里五次面試,四月順利收到實(shí)習(xí)。因?yàn)槲倚睦砗芮宄?,我的目?biāo)是阿里。所以在收到阿里之后的那晚,我重新規(guī)劃了接下來的學(xué)習(xí)計(jì)劃,將我的短期目標(biāo)更新成拿下阿里轉(zhuǎn)正。 我的2017是忙碌的一年,從年初備戰(zhàn)實(shí)習(xí)春招,年三十都在死磕JDK源碼,三月份經(jīng)歷了阿里五次面試,四月順利收到實(shí)習(xí)offer。然后五月懷著忐忑的心情開始了螞蟻金...
閱讀 3158·2021-11-22 09:34
閱讀 657·2021-11-22 09:34
閱讀 2516·2021-10-08 10:18
閱讀 3444·2021-09-22 15:57
閱讀 2698·2021-09-22 15:25
閱讀 2502·2019-08-30 15:54
閱讀 2253·2019-08-30 15:44
閱讀 1853·2019-08-29 11:18