鎖是 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)形式如下:
http://wiki.jikexueyuan.com/project/java-memory-model/images/24.png" alt="" />
在上圖中,每一個(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線程可見。
當(dāng)線程釋放鎖時(shí),JMM 會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。以上面的MonitorExample 程序?yàn)槔珹線程釋放鎖后,共享數(shù)據(jù)的狀態(tài)示意圖如下:
http://wiki.jikexueyuan.com/project/java-memory-model/images/25.png" alt="" />
當(dāng)線程獲取鎖時(shí),JMM 會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無(wú)效。從而使得被監(jiān)視器保護(hù)的臨界區(qū)代碼必須要從主內(nèi)存中去讀取共享變量。下面是鎖獲取的狀態(tài)示意圖:
http://wiki.jikexueyuan.com/project/java-memory-model/images/26.png" alt="" />
對(duì)比鎖釋放-獲取的內(nèi)存語(yǔ)義與 volatile 寫-讀的內(nèi)存語(yǔ)義,可以看出:鎖釋放與 volatile 寫有相同的內(nèi)存語(yǔ)義;鎖獲取與 volatile 讀有相同的內(nèi)存語(yǔ)義。
下面對(duì)鎖釋放和鎖獲取的內(nèi)存語(yǔ)義做個(gè)總結(jié):
本文將借助 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 的類圖(僅畫出與本文相關(guān)的部分):
http://wiki.jikexueyuan.com/project/java-memory-model/images/27.png" alt="" />
ReentrantLock 分為公平鎖和非公平鎖,我們首先分析公平鎖。
使用公平鎖時(shí),加鎖方法 lock() 的方法調(diào)用軌跡如下:
在第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)用軌跡如下:
在第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)存語(yǔ)義的實(shí)現(xiàn)。
非公平鎖的釋放和公平鎖完全一樣,所以這里僅僅分析非公平鎖的獲取。
使用非公平鎖時(shí),加鎖方法 lock() 的方法調(diào)用軌跡如下:
在第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ì)該方法的說(shuō)明如下:如果當(dāng)前狀態(tài)值等于預(yù)期值,則以原子方式將同步狀態(tài)設(shè)置為給定的更新值。此操作具有 volatile 讀和寫的內(nèi)存語(yǔ)義。
這里我們分別從編譯器和處理器的角度來(lái)分析,CAS 如何同時(shí)具有 volatile 讀和 volatile 寫的內(nèi)存語(yǔ)義。
前文我們提到過(guò),編譯器不會(huì)對(duì) volatile 讀與 volatile 讀后面的任意內(nèi)存操作重排序;編譯器不會(huì)對(duì) volatile 寫與 volatile 寫前面的任意內(nèi)存操作重排序。組合這兩個(gè)條件,意味著為了同時(shí)實(shí)現(xiàn) volatile 讀和 volatile 寫的內(nèi)存語(yǔ)義,編譯器不能對(duì) CAS 與 CAS 前面和后面的任意內(nèi)存操作重排序。
下面我們來(lái)分析在常見的 intel x86 處理器中,CAS 是如何同時(shí)具有 volatile 讀和 volatile 寫的內(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ō)明如下:
確保對(duì)內(nèi)存的讀-改-寫操作原子執(zhí)行。在 Pentium 及 Pentium 之前的處理器中,帶有l(wèi)ock 前綴的指令在執(zhí)行期間會(huì)鎖住總線,使得其他處理器暫時(shí)無(wú)法通過(guò)總線訪問(wèn)內(nèi)存。很顯然,這會(huì)帶來(lái)昂貴的開銷。從 Pentium 4,Intel Xeon 及 P6 處理器開始,intel 在原有總線鎖的基礎(chǔ)上做了一個(gè)很有意義的優(yōu)化:如果要訪問(wèn)的內(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ì)一直被鎖定,其它處理器無(wú)法讀/寫該指令要訪問(wèn)的內(nèi)存區(qū)域,因此能保證指令執(zhí)行的原子性。這個(gè)操作過(guò)程叫做緩存鎖定(cache locking),緩存鎖定將大大降低 lock 前綴指令的執(zhí)行開銷,但是當(dāng)多處理器之間的競(jìng)爭(zhēng)程度很高或者指令訪問(wèn)的內(nèi)存地址未對(duì)齊時(shí),仍然會(huì)鎖住總線。
禁止該指令與之前和之后的讀和寫指令重排序。
上面的第2點(diǎn)和第3點(diǎn)所具有的內(nèi)存屏障效果,足以同時(shí)實(shí)現(xiàn) volatile 讀和 volatile 寫的內(nèi)存語(yǔ)義。
經(jīng)過(guò)上面的這些分析,現(xiàn)在我們終于能明白為什么 JDK 文檔說(shuō) CAS 同時(shí)具有 volatile 讀和volatile 寫的內(nèi)存語(yǔ)義了。
現(xiàn)在對(duì)公平鎖和非公平鎖的內(nèi)存語(yǔ)義做個(gè)總結(jié):
從本文對(duì) ReentrantLock 的分析可以看出,鎖釋放-獲取的內(nèi)存語(yǔ)義的實(shí)現(xiàn)至少有下面兩種方式:
由于 java 的 CAS 同時(shí)具有 volatile 讀和 volatile 寫的內(nèi)存語(yǔ)義,因此 Java 線程之間的通信現(xiàn)在有了下面四種方式:
Java 的 CAS 會(huì)使用現(xiàn)代處理器上提供的高效機(jī)器級(jí)別原子指令,這些原子指令以原子方式對(duì)內(nèi)存執(zhí)行讀-改-寫操作,這是在多處理器中實(shí)現(xiàn)同步的關(guān)鍵(從本質(zhì)上來(lái)說(shuō),能夠支持原子性讀-改-寫指令的計(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)模式:
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)示意圖如下:
http://wiki.jikexueyuan.com/project/java-memory-model/images/28.png" alt="" />