程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 深入理解Java內存模型(五) 鎖

深入理解Java內存模型(五) 鎖

編輯:關於JAVA

鎖的釋放-獲取建立的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 關系可以分為兩類:

根據程序次序規則,1 happens before 2, 2 happens before 3; 4 happens before 5, 5 happens before 6。

根據監視器鎖規則,3 happens before 4。

根據happens before 的傳遞性,2 happens before 5。

上述happens before 關系的圖形化表現形式如下:

在上圖中,每一個箭頭鏈接的兩個節點,代表了一個happens before 關系。黑色箭頭表示程序順序規 則;橙色箭頭表示監視器鎖規則;藍色箭頭表示組合這些規則後提供的happens before保證。

上 圖表示在線程A釋放了鎖之後,隨後線程B獲取同一個鎖。在上圖中,2 happens before 5。因此,線程A 在釋放鎖之前所有可見的共享變量,在線程B獲取同一個鎖之後,將立刻變得對B線程可見。

鎖釋 放和獲取的內存語義

當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存 中。以上面的MonitorExample程序為例,A線程釋放鎖後,共享數據的狀態示意圖如下:

當線程獲取鎖時,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內存語義實現的關鍵。 下面是ReentrantLock的類圖(僅畫出與本文相關的部分):

查看本欄目

ReentrantLock分為公平鎖和非公平鎖,我們首先分析公平鎖。

使用公平鎖時,加鎖方法lock ()的方法調用軌跡如下:

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。

在使用公平鎖時 ,解鎖方法unlock()的方法調用軌跡如下:

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;在獲取鎖時首先讀這個volatile變量。根據volatile的happens-before 規則,釋放鎖的線程在寫volatile變量之前可見的共享變量,在獲取鎖的線程讀取同一個volatile變量後 將立即變的對獲取鎖的線程可見。

現在我們分析非公平鎖的內存語義的實現。

非公平鎖的釋放和公平鎖完全一樣,所以這裡僅僅 分析非公平鎖的獲取。

使用公平鎖時,加鎖方法lock()的方法調用軌跡如下:

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()方法調用簡稱為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前綴的說明如下:

確保對內存的讀-改-寫操作原子執行。在Pentium及Pentium之前的處理器中,帶有lock前綴的指令在 執行期間會鎖住總線,使得其他處理器暫時無法通過總線訪問內存。很顯然,這會帶來昂貴的開銷。從 Pentium 4,Intel Xeon及P6處理器開始,intel在原有總線鎖的基礎上做了一個很有意義的優化:如果要 訪問的內存區域(area of memory)在lock前綴指令執行期間已經在處理器內部的緩存中被鎖定(即包含 該內存區域的緩存行當前處於獨占或以修改狀態),並且該內存區域被完全包含在單個緩存行(cache line)中,那麼處理器將直接執行該指令。由於在指令執行期間該緩存行會一直被鎖定,其它處理器無法 讀/寫該指令要訪問的內存區域,因此能保證指令執行的原子性。這個操作過程叫做緩存鎖定(cache locking),緩存鎖定將大大降低lock前綴指令的執行開銷,但是當多處理器之間的競爭程度很高或者指 令訪問的內存地址未對齊時,仍然會鎖住總線。

禁止該指令與之前和之後的讀和寫指令重排序。

把寫緩沖區中的所有數據刷新到內存中。

上面的第2點和第3點所具有的內存屏障效果,足以同時實現volatile讀和volatile寫的內存語義。

經過上面的這些分析,現在我們終於能明白為什麼JDK文檔說CAS同時具有volatile讀和volatile 寫的內存語義了。

現在對公平鎖和非公平鎖的內存語義做個總結:

公平鎖和非公平鎖釋放時,最後都要寫一個volatile變量state。

公平鎖獲取時,首先會去讀這個volatile變量。

非公平鎖獲取時,首先會用CAS更新這個volatile變量,這個操作同時具有volatile讀和volatile寫的 內存語義。

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

利用volatile變量的寫-讀所具有的內存語義。

利用CAS所附帶的volatile讀和volatile寫的內存語義。

concurrent包的實現

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

A線程寫volatile變量,隨後B線程讀這個volatile變量。

A線程寫volatile變量,隨後B線程用CAS更新這個volatile變量。

A線程用CAS更新一個volatile變量,隨後B線程用CAS更新這個volatile變量。

A線程用CAS更新一個volatile變量,隨後B線程讀這個volatile變量。

Java的CAS會使用現代處理器上提供的高效機器級別原子指令,這些原子指令以原子方式對內存執行讀 -改-寫操作,這是在多處理器中實現同步的關鍵(從本質上來說,能夠支持原子性讀-改-寫指令的計算機 器,是順序計算圖靈機的異步等價機器,因此任何現代的多處理器都會去支持某種能對內存執行原子性讀 -改-寫操作的原子指令)。同時,volatile變量的讀/寫和CAS可以實現線程之間的通信。把這些特性整合 在一起,就形成了整個concurrent包得以實現的基石。如果我們仔細分析concurrent包的源代碼實現,會 發現一個通用化的實現模式:

首先,聲明共享變量為volatile;

然後,使用CAS的原子條件更新來實現線程之間的同步;

同時,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內存語義來實現線程之間的通信。

AQS,非阻塞數據結構和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包 中的基礎類都是使用這種模式來實現的,而concurrent包中的高層類又是依賴於這些基礎類來實現的。從 整體來看,concurrent包的實現示意圖如下:

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved