1. 程式人生 > >Java中鎖的實現與記憶體語義

Java中鎖的實現與記憶體語義

1. 概述

鎖在實際使用時只是明白鎖限制了併發訪問, 但是鎖是如何實現併發訪問的, 同學們可能不太清楚, 下面這篇文章就來揭開鎖的神祕面紗.

2. 鎖的記憶體語義

  • 當執行緒獲取鎖時, JMM會把執行緒對應的本地記憶體置為無效. 從而使得被監視器保護的臨界區的變數必須從主記憶體中讀取.
  • 當執行緒釋放鎖時, JMM會把該執行緒對應的本地記憶體中的共享變數重新整理到主記憶體中(並不是不釋放鎖就不重新整理到主記憶體, 只是釋放鎖時把未重新整理到主記憶體中的資料重新整理到主記憶體).

鎖的記憶體語義與volatile的記憶體語義

  • 鎖獲取與volatile讀有相同的記憶體語義.
  • 鎖釋放與volatile寫有相同的記憶體語義.

記憶體語義總結

  • 執行緒A釋放一個鎖, 實質上是執行緒A向接下來將要獲取這個鎖的某個執行緒發出了(執行緒A對共享變數所做修改的)訊息.
  • 執行緒B獲取一個鎖, 實質上是執行緒B接收了之前某個執行緒發出的(在釋放這個鎖之前對共享變數所做修改的)訊息.
  • 執行緒A釋放鎖, 隨後執行緒B獲取這個鎖, 這個過程實質上是執行緒A通過主記憶體向執行緒B傳送訊息.

3. 鎖記憶體語義的實現

下面以ReentrantLock為例, 獲取到鎖就是把state改為1(不考慮重入), 釋放鎖時改為0.

而加鎖的關鍵程式碼就是

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 var1, long var2, int var4, int var5);

可以看到, 這是一個本地方法呼叫. 這個本地方法在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寫的記憶體語義了.

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

  1. 利用volatile變數的寫-讀所具有的記憶體語義.
  2. 利用CAS所附帶的volatile讀和volatile寫的記憶體語義.

4. 總結

對於鎖, 可以這麼理解, N個執行緒去通過CAS去修改一個volatile變數, 但是由於CPU提供的機制, 只能有一個執行緒修改成功, 修改成功的執行緒獲得鎖, 其它執行緒以及後來的執行緒要麼自旋一會兒, 要麼直接掛起, 等待獲取鎖的執行緒釋放鎖時去喚醒. 就是這麼個過程.

-------------------------------------------
不要因為知識簡單就忽略, 不積跬步無以至千里.

鄭州市不孕不育醫院

鄭州哪個醫院治療不孕不育好

鄭州不孕不育哪家好

鄭州不孕不育