Java內存模型-鎖(3)

前言

衆所周知,鎖可以讓臨界區互斥執行。這裏將介紹鎖的另一個同樣重要,但常常被忽視的功能:鎖的內存語義。

鎖的釋放-獲取建立的happens-before關係

鎖是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關係可以分爲3類。

1)根據程序次序規則,1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happens- before 6。
2)根據監視器鎖規則,3 happens-before 4。
3)根據happens-before的傳遞性,2 happens-before 5。
在這裏插入圖片描述

在上圖中,每一個箭頭鏈接的兩個節點,代表了一個happens-before關係。黑色箭頭表示 程序順序規則;橙色箭頭表示監視器鎖規則;藍色箭頭表示組合這些規則後提供的happens- before保證。 上圖表示在線程A釋放了鎖之後,隨後線程B獲取同一個鎖。在上圖中,2 happens-before 5。因此,線程A在釋放鎖之前所有可見的共享變量,在線程B獲取同一個鎖之後,將立刻變得對B線程可見。

鎖的釋放和獲取的內存語義

當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中。當線程獲取鎖時,JMM會把該線程對應的本地內存置爲無效。從而使得被監視器保護的 臨界區代碼必須從主內存中讀取共享變量。對比鎖釋放-獲取的內存語義與volatile寫-讀的內存語義可以看出:鎖釋放與volatile寫有 相同的內存語義;鎖獲取與volatile讀有相同的內存語義。
下面對鎖釋放和鎖獲取的內存語義做個總結。

·線程A釋放一個鎖,實質上是線程A向接下來將要獲取這個鎖的某個線程發出了(線程A 對共享變量所做修改的)消息。
·線程B獲取一個鎖,實質上是線程B接收了之前某個線程發出的(在釋放這個鎖之前對共 享變量所做修改的)消息。
·線程A釋放鎖,隨後線程B獲取這個鎖,這個過程實質上是線程A通過主內存向線程B發 送消息。

鎖內存語義的實現

本文將藉助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內存語義實現的關鍵。 圖3-27是ReentrantLock的類圖(僅畫出與本文相關的部分)。
在這裏插入圖片描述
圖3-27 ReentrantLock的類圖
ReentrantLock分爲公平鎖和非公平鎖,我們首先分析公平鎖。 使用公平鎖時,加鎖方法lock()調用軌跡如下。

1)ReentrantLock:lock()。
2)FairSync:lock()。
3)AbstractQueuedSynchronizer:acquire(int arg)。
4)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。 在使用公平鎖時,解鎖方法unlock()調用軌跡如下。

1)ReentrantLock:unlock()。
2)AbstractQueuedSynchronizer:release(int arg)。
3)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,在獲取鎖時首先讀這個volatile變量。根據 volatile的happens-before規則,釋放鎖的線程在寫volatile變量之前可見的共享變量,在獲取鎖 的線程讀取同一個volatile變量後將立即變得對獲取鎖的線程可見。 現在我們來分析非公平鎖的內存語義的實現。非公平鎖的釋放和公平鎖完全一樣,所以 這裏僅僅分析非公平鎖的獲取。使用非公平鎖時,加鎖方法lock()調用軌跡如下。

1)ReentrantLock:lock()。
2)NonfairSync:lock()。
3)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()方法調用簡稱爲 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和atomic_windows_x86.inline.hpp。這個本地方法的最終實現在openjdk的 如下位置:openjdk-7-fcs-src-b147- 27_jun_2011\openjdk\hotspot\src\os_cpu\windows_x86\vm\atomic_windows_x86.inline.hpp(對應於 Windows操作系統,X86處理器)。下面是對應於intel X86處理器的源代碼的片段。

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前綴的說明如下。

1)確保對內存的讀-改-寫操作原子執行。在Pentium及Pentium之前的處理器中,帶有lock前綴的指令在執行期間會鎖住總線,使得其他處理器暫時無法通過總線訪問內存。很顯然,這會 帶來昂貴的開銷。從Pentium 4、Intel Xeon及P6處理器開始,Intel使用緩存鎖(Cache Locking) 來保證指令執行的原子性。緩存鎖定將大大降低lock前綴指令的執行開銷。
2)禁止該指令,與之前和之後的讀和寫指令重排序。
3)把寫緩衝區中的所有數據刷新到內存中。

上面的第2點和第3點所具有的內存屏障效果,足以同時實現volatile讀和volatile寫的內存語義。經過上面的分析,現在我們終於能明白爲什麼JDK文檔說CAS同時具有volatile讀和 volatile寫的內存語義了。 現在對公平鎖和非公平鎖的內存語義做個總結。

·公平鎖和非公平鎖釋放時,最後都要寫一個volatile變量state。
·公平鎖獲取時,首先會去讀volatile變量。
·非公平鎖獲取時,首先會用CAS更新volatile變量,這個操作同時具有volatile讀和volatile寫的內存語義。

從本文對ReentrantLock的分析可以看出,鎖釋放-獲取的內存語義的實現至少有下面兩種方式。

1)利用volatile變量的寫-讀所具有的內存語義。
2)利用CAS所附帶的volatile讀和volatile寫的內存語義。

concurrent包的實現

由於Java的CAS同時具有volatile讀和volatile寫的內存語義,因此Java線程之間的通信現 在有了下面4種方式。

1)A線程寫volatile變量,隨後B線程讀這個volatile變量。
2)A線程寫volatile變量,隨後B線程用CAS更新這個volatile變量。
3)A線程用CAS更新一個volatile變量,隨後B線程用CAS更新這個volatile變量。
4)A線程用CAS更新一個volatile變量,隨後B線程讀這個volatile變量。

Java的CAS會使用現代處理器上提供的高效機器級別的原子指令,這些原子指令以原子 方式對內存執行讀-改-寫操作,這是在多處理器中實現同步的關鍵(從本質上來說,能夠支持 原子性讀-改-寫指令的計算機,是順序計算圖靈機的異步等價機器,因此任何現代的多處理器 都會去支持某種能對內存執行原子性讀-改-寫操作的原子指令)。同時,volatile變量的讀/寫和CAS可以實現線程之間的通信。把這些特性整合在一起,就形成了整個concurrent包得以實現 的基石。如果我們仔細分析concurrent包的源代碼實現,會發現一個通用化的實現式。 首先,聲明共享變量爲volatile。然後,使用CAS的原子條件更新來實現線程之間的同步。同時,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內存語義來實現線程之間的 通信。AQS,非阻塞數據結構和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包中的基礎類都是使用這種模式來實現的,而concurrent包中的高層類又是依賴於這些基礎類來實現的。從整體看,concurrent包的實現如下圖所示。
在這裏插入圖片描述

先贊後看,養成習慣。歡迎收看一個行走的熊貓程序猿,下期再見

關注
文章持續更新,可以微信搜索「 熊貓程序猿a 」第一時間催更
公衆號

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章