鎖是java併發編程中最重要的同步機制。鎖除了讓臨界區互斥執行外,還可讓釋放鎖的線程向獲取同一個鎖的線程發送消息。下面是鎖釋放-獲取的示例代碼:java
class MonitorExample { int a = 0; public synchronized void writer() { //1 a++; //2 } //3 public synchronized void reader() { //4 int i = a; //5 …… } //6 }
假設線程A執行writer()方法,隨後線程B執行reader()方法。根據happens before規則,這個過程包含的happens before 關係能夠分爲兩類:c++
上述happens before 關係的圖形化表現形式以下:編程
在上圖中,每個箭頭連接的兩個節點,表明了一個happens before 關係。黑色箭頭表示程序順序規則;橙色箭頭表示監視器鎖規則;藍色箭頭表示組合這些規則後提供的happens before保證。windows
上圖表示在線程A釋放了鎖以後,隨後線程B獲取同一個鎖。在上圖中,2 happens before 5。所以,線程A在釋放鎖以前全部可見的共享變量,在線程B獲取同一個鎖以後,將馬上變得對B線程可見。緩存
當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中。以上面的MonitorExample程序爲例,A線程釋放鎖後,共享數據的狀態示意圖以下:數據結構
當線程獲取鎖時,JMM會把該線程對應的本地內存置爲無效。從而使得被監視器保護的臨界區代碼必需要從主內存中去讀取共享變量。下面是鎖獲取的狀態示意圖:併發
對比鎖釋放-獲取的內存語義與volatile寫-讀的內存語義,能夠看出:鎖釋放與volatile寫有相同的內存語義;鎖獲取與volatile讀有相同的內存語義。app
下面對鎖釋放和鎖獲取的內存語義作個總結:框架
本文將藉助ReentrantLock的源代碼,來分析鎖內存語義的具體實現機制。異步
請看下面的示例代碼:
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中,調用lock()方法獲取鎖;調用unlock()方法釋放鎖。
ReentrantLock的實現依賴於java同步器框架AbstractQueuedSynchronizer(本文簡稱之爲AQS)。AQS使用一個整型的volatile變量(命名爲state)來維護同步狀態,立刻咱們會看到,這個volatile變量是ReentrantLock內存語義實現的關鍵。 下面是ReentrantLock的類圖(僅畫出與本文相關的部分):
ReentrantLock分爲公平鎖和非公平鎖,咱們首先分析公平鎖。
使用公平鎖時,加鎖方法lock()的方法調用軌跡以下:
在第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。
在使用公平鎖時,解鎖方法unlock()的方法調用軌跡以下:
在第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;在獲取鎖時首先讀這個volatile變量。根據volatile的happens-before規則,釋放鎖的線程在寫volatile變量以前可見的共享變量,在獲取鎖的線程讀取同一個volatile變量後將當即變的對獲取鎖的線程可見。
如今咱們分析非公平鎖的內存語義的實現。
非公平鎖的釋放和公平鎖徹底同樣,因此這裏僅僅分析非公平鎖的獲取。
使用非公平鎖時,加鎖方法lock()的方法調用軌跡以下:
在第3步真正開始加鎖,下面是該方法的源代碼:
protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
該方法以原子操做的方式更新state變量,本文把java的compareAndSet()方法調用簡稱爲CAS。JDK文檔對該方法的說明以下:若是當前狀態值等於預期值,則以原子方式將同步狀態設置爲給定的更新值。此操做具備 volatile 讀和寫的內存語義。
這裏咱們分別從編譯器和處理器的角度來分析,CAS如何同時具備volatile讀和volatile寫的內存語義。
前文咱們提到過,編譯器不會對volatile讀與volatile讀後面的任意內存操做重排序;編譯器不會對volatile寫與volatile寫前面的任意內存操做重排序。組合這兩個條件,意味着爲了同時實現volatile讀和volatile寫的內存語義,編譯器不能對CAS與CAS前面和後面的任意內存操做重排序。
下面咱們來分析在常見的intel x86處理器中,CAS是如何同時具備volatile讀和volatile寫的內存語義的。
下面是sun.misc.Unsafe類的compareAndSwapInt()方法的源代碼:
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
能夠看到這是個本地方法調用。這個本地方法在openjdk中依次調用的c++代碼爲:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp。這個本地方法的最終實如今openjdk的以下位置:openjdk-7-fcs-src-b147-27jun2011\openjdk\hotspot\src\oscpu\windowsx86\vm\ atomicwindowsx86.inline.hpp(對應於windows操做系統,X86處理器)。下面是對應於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 } }
如上面源代碼所示,程序會根據當前處理器的類型來決定是否爲cmpxchg指令添加lock前綴。若是程序是在多處理器上運行,就爲cmpxchg指令加上lock前綴(lock cmpxchg)。反之,若是程序是在單處理器上運行,就省略lock前綴(單處理器自身會維護單處理器內的順序一致性,不須要lock前綴提供的內存屏障效果)。
intel的手冊對lock前綴的說明以下:
上面的第2點和第3點所具備的內存屏障效果,足以同時實現volatile讀和volatile寫的內存語義。
通過上面的這些分析,如今咱們終於能明白爲何JDK文檔說CAS同時具備volatile讀和volatile寫的內存語義了。
如今對公平鎖和非公平鎖的內存語義作個總結:
從本文對ReentrantLock的分析能夠看出,鎖釋放-獲取的內存語義的實現至少有下面兩種方式:
因爲java的CAS同時具備 volatile 讀和volatile寫的內存語義,所以Java線程之間的通訊如今有了下面四種方式:
Java的CAS會使用現代處理器上提供的高效機器級別原子指令,這些原子指令以原子方式對內存執行讀-改-寫操做,這是在多處理器中實現同步的關鍵(從本質上來講,可以支持原子性讀-改-寫指令的計算機器,是順序計算圖靈機的異步等價機器,所以任何現代的多處理器都會去支持某種能對內存執行原子性讀-改-寫操做的原子指令)。同時,volatile變量的讀/寫和CAS能夠實現線程之間的通訊。把這些特性整合在一塊兒,就造成了整個concurrent包得以實現的基石。若是咱們仔細分析concurrent包的源代碼實現,會發現一個通用化的實現模式:
AQS,非阻塞數據結構和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包中的基礎類都是使用這種模式來實現的,而concurrent包中的高層類又是依賴於這些基礎類來實現的。從總體來看,concurrent包的實現示意圖以下: