Java中的synchronized鎖和Lock鎖的內存語義

Java中的鎖包括synchronized鎖和lock鎖。他們都能保證鎖的內存語義正確的實現,但是他們的底層原理卻是不一樣的,Lock鎖的底層是使用volatile和CAS的內存語義來實現鎖的內存語義的,而synchronized用的鎖是存在java對象頭裏的,是基於JVM的支持來實現的。

1 鎖的內存語義

  鎖是Java併發編程中最重要的同步機制。鎖除了讓臨界區互斥執行外,還可以讓釋放鎖的線程向獲取同一個鎖的線程發送消息。

語義:
  當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中。鎖釋放與volatile寫具有相同的內存語義。
  當線程獲取鎖時,JMM 會把該線程對應的本地內存置爲無效。從而使得被監視器保護的臨界區代碼必須要從主內存中去讀取共享變量。鎖獲取與volatile讀具有相同的內存語義。

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

2 Lock鎖內存語義的實現

  以ReentrantLock爲例,在ReentrantLock中,調用lock()方法獲取鎖;調用unlock()方法釋放鎖。
  ReentrantLock的實現依賴於Java同步器框架AbstractQueuedSynchronizer(本文簡稱之爲AQS)。AQS使用一個整型的volatile變量(命名爲state)來維護同步狀態,這個volatile變量是ReentrantLock內存語義實現的關鍵。
  ReentrantLock分爲公平鎖和非公平鎖.我們首先分析公平鎖。
  使用公平鎖時,加鎖方法lock()調用軌跡如下:

  1. ReentrantLock:lock()
  2. FairSync:lock()
  3. AbstractQueuedSynchronizer:acquire(int arg)
  4. FairSync:tryAcquire(int acquires)

  在第4步真正開始加鎖,下面是該方法的部分源代碼:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 獲取鎖的開始,首先讀volatile變量state
    int c = getState();
     //..............

  從上面源代碼中我們可以看出,加鎖方法首先讀volatile變量state。
  在使用公平鎖時,解鎖方法unlock()調用軌跡如下:

  1. ReentrantLock:unlock()
  2. AbstractQueuedSynchronizer:release(intarg)
  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);
    }
    // 釋放鎖的最後,寫volatile變量state
    setState(c);     
    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寫的內存語義。

2.1 CAS語義編譯器實現

  編譯器不會對volatile讀與volatile讀後面的任意內存操作重排序;編譯器不會對volatile寫與volatile寫前面的任意內存操作重排序。組合這兩個條件,意味着爲了同時實現volatile讀和volatile寫的內存語義,編譯器不能對CAS與CAS前面和後面的任意內存操作重排序。

2.2 CAS語義處理器實現

  與volatile一樣,處理器會在cas方法指令前自動加上lock前綴指令,lock前綴指令可以

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

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

2.3 Lock鎖總結

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

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

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

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

3 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可以實現線程之間的通信。把CAS和volatile的這些特性整合在一起,就形成了整個concurrent包得以實現的基石。 如果我們仔細分析concurrent包的源代碼實現,會發現一個通用化的實現模式:

1、首先,聲明共享變量爲volatile;
2、然後,使用CAS的原子條件更新來實現線程之間的同步;
3、同時,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內存語義來實現線程之間的通信。

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

4 總結

  本文介紹了鎖的內存語義,以及我們的Lock鎖如何使用CAS和volatile的內存語義來實現鎖的內存語義,以及JUC包的實現基石——CAS和volatile。同時Lock鎖比synchronized更加的靈活,關於synchronized和Lock的詳細實現原理,將在後面的博文中一一展現。

參考
《JSR133規範》
《Java併發編程之美》
《實戰Java高併發程序設計》
《Java併發編程的藝術》

如果有什麼不懂或者需要交流,可以留言。另外希望點贊、收藏、關注,我將不間斷更新各種Java學習博客!

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