鎖的釋放-獲取建立的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è)過(guò)程包含的happens before 關(guān)系可以分為兩類:
上述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在釋放鎖之前所有可見(jiàn)的共享變量,在線程B獲取同一個(gè)鎖之后,將立刻變得對(duì)B線程可見(jiàn)。 鎖釋放和獲取的內(nèi)存語(yǔ)義當(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)存置為無(wú)效。從而使得被監(jiān)視器保護(hù)的臨界區(qū)代碼必須要從主內(nèi)存中去讀取共享變量。下面是鎖獲取的狀態(tài)示意圖: 對(duì)比鎖釋放-獲取的內(nèi)存語(yǔ)義與volatile寫(xiě)-讀的內(nèi)存語(yǔ)義,可以看出:鎖釋放與volatile寫(xiě)有相同的內(nèi)存語(yǔ)義;鎖獲取與volatile讀有相同的內(nèi)存語(yǔ)義。 下面對(duì)鎖釋放和鎖獲取的內(nèi)存語(yǔ)義做個(gè)總結(jié):
鎖內(nèi)存語(yǔ)義的實(shí)現(xiàn)本文將借助ReentrantLock的源代碼,來(lái)分析鎖內(nèi)存語(yǔ)義的具體實(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)來(lái)維護(hù)同步狀態(tài),馬上我們會(huì)看到,這個(gè)volatile變量是ReentrantLock內(nèi)存語(yǔ)義實(shí)現(xiàn)的關(guān)鍵。 下面是ReentrantLock的類圖(僅畫(huà)出與本文相關(guān)的部分): ReentrantLock分為公平鎖和非公平鎖,我們首先分析公平鎖。 使用公平鎖時(shí),加鎖方法lock()的方法調(diào)用軌跡如下:
在第4步真正開(kāi)始加鎖,下面是該方法的源代碼: protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //獲取鎖的開(kāi)始,首先讀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)用軌跡如下:
在第3步真正開(kāi)始釋放鎖,下面是該方法的源代碼: 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); //釋放鎖的最后,寫(xiě)volatile變量state return free; } 從上面的源代碼我們可以看出,在釋放鎖的最后寫(xiě)volatile變量state。 公平鎖在釋放鎖的最后寫(xiě)volatile變量state;在獲取鎖時(shí)首先讀這個(gè)volatile變量。根據(jù)volatile的happens-before規(guī)則,釋放鎖的線程在寫(xiě)volatile變量之前可見(jiàn)的共享變量,在獲取鎖的線程讀取同一個(gè)volatile變量后將立即變的對(duì)獲取鎖的線程可見(jiàn)。 現(xiàn)在我們分析非公平鎖的內(nèi)存語(yǔ)義的實(shí)現(xiàn)。 非公平鎖的釋放和公平鎖完全一樣,所以這里僅僅分析非公平鎖的獲取。 使用公平鎖時(shí),加鎖方法lock()的方法調(diào)用軌跡如下:
在第3步真正開(kāi)始加鎖,下面是該方法的源代碼: 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ì)該方法的說(shuō)明如下:如果當(dāng)前狀態(tài)值等于預(yù)期值,則以原子方式將同步狀態(tài)設(shè)置為給定的更新值。此操作具有 volatile 讀和寫(xiě)的內(nèi)存語(yǔ)義。 這里我們分別從編譯器和處理器的角度來(lái)分析,CAS如何同時(shí)具有volatile讀和volatile寫(xiě)的內(nèi)存語(yǔ)義。 前文我們提到過(guò),編譯器不會(huì)對(duì)volatile讀與volatile讀后面的任意內(nèi)存操作重排序;編譯器不會(huì)對(duì)volatile寫(xiě)與volatile寫(xiě)前面的任意內(nèi)存操作重排序。組合這兩個(gè)條件,意味著為了同時(shí)實(shí)現(xiàn)volatile讀和volatile寫(xiě)的內(nèi)存語(yǔ)義,編譯器不能對(duì)CAS與CAS前面和后面的任意內(nèi)存操作重排序。 下面我們來(lái)分析在常見(jiàn)的intel x86處理器中,CAS是如何同時(shí)具有volatile讀和volatile寫(xiě)的內(nèi)存語(yǔ)義的。 下面是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和atomicwindowsx86.inline.hpp。這個(gè)本地方法的最終實(shí)現(xiàn)在openjdk的如下位置:openjdk-7-fcs-src-b147-27jun2011\openjdk\hotspot\src\oscpu\windowsx86\vm\ atomicwindowsx86.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)前處理器的類型來(lái)決定是否為cmpxchg指令添加lock前綴。如果程序是在多處理器上運(yùn)行,就為cmpxchg指令加上lock前綴(lock cmpxchg)。反之,如果程序是在單處理器上運(yùn)行,就省略lock前綴(單處理器自身會(huì)維護(hù)單處理器內(nèi)的順序一致性,不需要lock前綴提供的內(nèi)存屏障效果)。 intel的手冊(cè)對(duì)lock前綴的說(shuō)明如下:
上面的第2點(diǎn)和第3點(diǎn)所具有的內(nèi)存屏障效果,足以同時(shí)實(shí)現(xiàn)volatile讀和volatile寫(xiě)的內(nèi)存語(yǔ)義。 經(jīng)過(guò)上面的這些分析,現(xiàn)在我們終于能明白為什么JDK文檔說(shuō)CAS同時(shí)具有volatile讀和volatile寫(xiě)的內(nèi)存語(yǔ)義了。 現(xiàn)在對(duì)公平鎖和非公平鎖的內(nèi)存語(yǔ)義做個(gè)總結(jié):
從本文對(duì)ReentrantLock的分析可以看出,鎖釋放-獲取的內(nèi)存語(yǔ)義的實(shí)現(xiàn)至少有下面兩種方式:
concurrent包的實(shí)現(xiàn)由于java的CAS同時(shí)具有 volatile 讀和volatile寫(xiě)的內(nèi)存語(yǔ)義,因此Java線程之間的通信現(xiàn)在有了下面四種方式:
Java的CAS會(huì)使用現(xiàn)代處理器上提供的高效機(jī)器級(jí)別原子指令,這些原子指令以原子方式對(duì)內(nèi)存執(zhí)行讀-改-寫(xiě)操作,這是在多處理器中實(shí)現(xiàn)同步的關(guān)鍵(從本質(zhì)上來(lái)說(shuō),能夠支持原子性讀-改-寫(xiě)指令的計(jì)算機(jī)器,是順序計(jì)算圖靈機(jī)的異步等價(jià)機(jī)器,因此任何現(xiàn)代的多處理器都會(huì)去支持某種能對(duì)內(nèi)存執(zhí)行原子性讀-改-寫(xiě)操作的原子指令)。同時(shí),volatile變量的讀/寫(xiě)和CAS可以實(shí)現(xiàn)線程之間的通信。把這些特性整合在一起,就形成了整個(gè)concurrent包得以實(shí)現(xiàn)的基石。如果我們仔細(xì)分析concurrent包的源代碼實(shí)現(xiàn),會(huì)發(fā)現(xiàn)一個(gè)通用化的實(shí)現(xiàn)模式:
AQS,非阻塞數(shù)據(jù)結(jié)構(gòu)和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包中的基礎(chǔ)類都是使用這種模式來(lái)實(shí)現(xiàn)的,而concurrent包中的高層類又是依賴于這些基礎(chǔ)類來(lái)實(shí)現(xiàn)的。從整體來(lái)看,concurrent包的實(shí)現(xiàn)示意圖如下: 參考文獻(xiàn)關(guān)于作者程曉明,Java軟件工程師,國(guó)家認(rèn)證的系統(tǒng)分析師、信息項(xiàng)目管理師。專注于并發(fā)編程,就職于富士通南大。個(gè)人郵箱:http://www./cn/articles/mailto:asst2003@163.com。 社區(qū)評(píng)論 Watch Thread
cas和volatile關(guān)系 三月 19, 2013 09:59by 感謝作者,幫我把之前零散的知識(shí)串起來(lái)。
我理解:cas包括兩部分 1.lock:保證具體‘volatile內(nèi)存語(yǔ)義’ 2.cmpxchg:保證原子操作 -> lock是volatile在intel平臺(tái)的具體實(shí)現(xiàn),對(duì)嗎 ? -> 若上述不成了,是說(shuō)cas自身就具體1、2特性,若去掉‘private volatile int state;’volatile,是否仍滿足 ? 引用“從整體來(lái)看,concurrent包的實(shí)現(xiàn)示意圖如下”,是把‘volatile’和‘cas’是放在同一層。 謝謝 Re: cas和volatile關(guān)系 三月 20, 2013 12:12by 引用“從整體來(lái)看,concurrent包的實(shí)現(xiàn)示意圖如下”,是把‘volatile’和‘cas’是放在同一層。
--把‘volatile的讀/寫(xiě)’和‘CAS’是放在同一層,是想回應(yīng)第一章提到的:“并發(fā)編程中,我們需要處理兩個(gè)關(guān)鍵問(wèn)題:線程之間如何通信及線程之間如何同步” java的concurrent包通過(guò)volatile的讀/寫(xiě),及CAS所具有的volatile讀和volatile寫(xiě)的內(nèi)存語(yǔ)義,來(lái)實(shí)現(xiàn)線程之間的通信。 java的concurrent包使用CAS來(lái)實(shí)現(xiàn)線程之間的同步。 鎖與共享內(nèi)存的疑問(wèn) 三月 20, 2013 03:08by 1.“當(dāng)線程獲取鎖時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無(wú)效。從而使得被監(jiān)視器保護(hù)的臨界區(qū)代碼必須要從主內(nèi)存中去讀取共享變量?!?br>2.“當(dāng)線程釋放鎖時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。”
從文中提到的這2點(diǎn),我的理解是這樣的: 在臨界區(qū)內(nèi):共享變量只能從主內(nèi)存中讀?。还蚕碜兞康膶?xiě)操作只能在本地內(nèi)存中進(jìn)行,因?yàn)樵陔x開(kāi)臨界區(qū)的時(shí)候本地內(nèi)存中的共享變量需要被刷新到主內(nèi)存中,如果寫(xiě)操作直接是寫(xiě)入主內(nèi)存,那么最終會(huì)因?yàn)楸镜貎?nèi)存和主內(nèi)存中的共享變量不一致而導(dǎo)致主內(nèi)存中的共享變量沒(méi)有得到應(yīng)有的更新。 另外我有一個(gè)疑問(wèn):將本地內(nèi)存的共享變量刷新到主內(nèi)存中的時(shí)候,是刷新本地內(nèi)存中的所有共享變量還是只刷新那些發(fā)生過(guò)寫(xiě)操作的共享變量? Re: 鎖與共享內(nèi)存的疑問(wèn) 三月 23, 2013 11:03by --離開(kāi)臨界區(qū)的時(shí)候本地內(nèi)存中的共享變量需要被刷新到主內(nèi)存中.
這是鎖內(nèi)存語(yǔ)義的需要。 如果當(dāng)前線程A在離開(kāi)臨界區(qū)時(shí)(釋放鎖時(shí)),還不把本地內(nèi)存鐘的共享變量刷新到主內(nèi)存; 那么接下來(lái)獲取這個(gè)鎖的線程B,將無(wú)法讀到線程A對(duì)共享變量所做的修改。 因此,“離開(kāi)臨界區(qū)的時(shí)候,本地內(nèi)存中的共享變量需要被刷新到主內(nèi)存中”,是鎖通信機(jī)制(鎖內(nèi)存語(yǔ)義)的需要。 --是刷新本地內(nèi)存中的所有共享變量還是只刷新那些發(fā)生過(guò)寫(xiě)操作的共享變量? 個(gè)人覺(jué)得,可以理解為只刷新那些發(fā)生過(guò)寫(xiě)操作的共享變量。 “本地內(nèi)存,主內(nèi)存,從主內(nèi)存中讀取變量,刷新共享變量到主內(nèi)存”,這些都是為了易于讀者理解而虛構(gòu)出來(lái)的。事實(shí)上,與JSR-133內(nèi)存模型相關(guān)的兩大規(guī)范都沒(méi)有提到這些概念: 1:《JSR-133: JavaTM Memory Model and Thread Specification》 2:《The Java? Language Specification Third Edition》的“17.4 Memory Model” 與JSR-133內(nèi)存模型相關(guān)的著作中,映像中好像只有Brian Goetz在下面這篇文章中提到了這些概念: http://www.ibm.com/developerworks/java/library/j-jtp03304/index.html 個(gè)人覺(jué)得這些概念形象,生動(dòng),易于讀者理解。所以就把它們借鑒到本文中來(lái)了:) 但我個(gè)人感覺(jué),使用這些概念也會(huì)有弊端。所以本文中,我只是簡(jiǎn)單提到它們。 對(duì)于JSR-133內(nèi)存模,重點(diǎn)應(yīng)該關(guān)注happens before ,以及編譯器重排序規(guī)則和處理器內(nèi)存屏障插入策略。 對(duì)鎖之前如何保證數(shù)據(jù)同步的問(wèn)題? 十一月 22, 2013 10:08by 非常感謝作者的這一系列文章,收貨蠻大。有一個(gè)疑問(wèn),在線程中對(duì)一個(gè)對(duì)象同步之前,如何保證線程內(nèi)的本地內(nèi)存寫(xiě)到主內(nèi)存中呢?因?yàn)榭戳宋恼?,同步鎖進(jìn)入前,本地內(nèi)存會(huì)無(wú)效,那么我對(duì)一個(gè)變量操作了,會(huì)寫(xiě)到主線程嗎?eg : Obj obj ; obj.x = 10; synchronized (obj) { // ... } |
|
來(lái)自: wlj2004 > 《java內(nèi)存模型》