1. 程式人生 > >深入Lock鎖底層原理

深入Lock鎖底層原理

當多個執行緒需要訪問某個公共資源的時候,我們知道需要通過加鎖來保證資源的訪問不會出問題。java提供了兩種方式來加鎖

一種是關鍵字:synchronized,一種是concurrent包下的lock鎖。

synchronized是java底層支援的,而concurrent包則是jdk實現

 

關於synchronized的原理可以閱讀再有人問你synchronized是什麼,就把這篇文章發給他。

在這裡,我會用盡可能少的程式碼,儘可能輕鬆的文字,儘可能多的圖來看看lock的原理。

我們以ReentrantLock為例做分析,其他原理類似。

我把這個過程比喻成一個做菜的過程,有什麼菜,做法如何?

我先列出lock實現過程中的幾個關鍵詞:計數值、雙向連結串列、CAS+自旋

 

我們以ReentrantLock為例做分析,其他原理類似。

    可以實現公平鎖和非公平鎖( 當有執行緒競爭鎖時,當前執行緒會首先嚐試獲得鎖而不是在佇列中進行排隊等候,這對於那些已經在佇列中排隊的執行緒來說顯得不公平,這也是非公平鎖的由來),預設情況下為非公平鎖。

實現原理

ReentrantLock() 幹了啥

  public ReentrantLock() {

        sync = new NonfairSync();

    }

在lock的建構函式中,定義了一個NonFairSync,

static final class NonfairSync extends Sync

NonfairSync 又是繼承於Sync

abstract static class Sync extends AbstractQueuedSynchronizer

 

一步一步往上找,找到了

這個鬼AbstractQueuedSynchronizer(簡稱AQS),最後這個鬼,又是繼承於AbstractOwnableSynchronizer(AOS),AOS主要是儲存獲取當前鎖的執行緒物件,程式碼不多不再展開。最後我們可以看到幾個主要類的繼承關係:

                                            

    FairSync 與 NonfairSync的區別在於,是不是保證獲取鎖的公平性,因為預設是NonfairSync(非公平性),我們以這個為例瞭解其背後的原理。

其他幾個類程式碼不多,最後的主要程式碼都是在AQS中,我們先看看這個類的主體結構。

看看AbstractQueuedSynchronizer是個什麼

再看看Node是什麼?

看到這裡的同學,是不是有種熱淚盈眶的感覺,這尼瑪,不就是雙向連結串列麼?我還記得第一次寫這個資料結構的時候,發現居然還有這麼神奇的一個東西。

最後我們可以發現鎖的儲存結構就兩個東西:"雙向連結串列" + "int型別狀態"。

簡單來說,ReenTrantLock的實現是一種自旋鎖,通過迴圈呼叫CAS操作來實現加鎖。它的效能比較好也是因為避免了使執行緒進入核心態的阻塞狀態。想盡辦法避免執行緒進入核心的阻塞狀態是我們去分析和理解鎖設計的關鍵鑰匙。

需要注意的是,他們的變數都被"transientvolatile修飾。

 

一個int值,一個雙向連結串列是如何烹飪處理鎖這道菜的呢,Doug Lea大神就是大神,

我們接下來看看,如何獲取鎖?

 

lock.lock()怎麼獲取鎖?

public void lock() {

    sync.lock();

}

可以看到呼叫的是,NonfairSync.lock()

看到這裡,我們基本有了一個大概的瞭解,還記得之前AQS中的int型別的state值,

這裡就是通過CAS(樂觀鎖)去修改state的值(鎖狀態值)。lock的基本操作還是通過樂觀鎖來實現的。

獲取鎖通過CAS,那麼沒有獲取到鎖,等待獲取鎖是如何實現的?我們可以看一下else分支的邏輯,acquire方法

public final void acquire(int arg) {

    if (!tryAcquire(arg) &&

        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

        selfInterrupt();

}

這裡幹了三件事情:

  • tryAcquire:會嘗試再次通過CAS獲取一次鎖。

  • addWaiter:將當前執行緒加入上面鎖的雙向連結串列(等待佇列)中

  • acquireQueued:通過自旋,判斷當前佇列節點是否可以獲取鎖。

 

addWaiter() 添加當前執行緒到等待連結串列中

可以看到,通過CAS確保能夠線上程安全的情況下,將當前執行緒加入到連結串列的尾部。

enq是個自旋+上述邏輯,有興趣的可以翻翻原始碼。

 

acquireQueued()    自旋+CAS嘗試獲取鎖

可以看到,噹噹前執行緒到頭部的時候,嘗試CAS更新鎖狀態,如果更新成功表示該等待執行緒獲取成功。從頭部移除。

 

每一個執行緒都在 自旋+CAS

最後簡要概括一下,獲取鎖的一個流程

 

lock.unlock() 釋放鎖

public void unlock() {

    sync.release(1);

}

可以看到呼叫的是,NonfairSync.release()

最後又呼叫了NonfairSync.tryRelease()

基本可以確認,釋放鎖就是對AQS中的狀態值State進行修改。同時更新下一個連結串列中的執行緒等待節點

 

 

總結

  • lock的儲存結構:一個int型別狀態值(用於鎖的狀態變更),一個雙向連結串列(用於儲存等待中的執行緒)
  • lock獲取鎖的過程:本質上是通過CAS來獲取狀態值修改,如果當場沒獲取到,會將該執行緒放線上程等待連結串列中。
  • lock釋放鎖的過程:修改狀態值,調整等待連結串列。

可以看到在整個實現過程中,lock大量使用CAS+自旋。因此根據CAS特性,lock建議使用在低鎖衝突的情況下。目前java1.6以後,官方對synchronized做了大量的鎖優化(偏向鎖、自旋、輕量級鎖)。因此在非必要的情況下,建議使用synchronized做同步操作。

最後,希望我的分析,能對你理解鎖的實現有所幫助。

____________________________________________________________________________

鎖實現

    簡單說來,AbstractQueuedSynchronizer會把所有的請求執行緒構成一個CLH佇列,當一個執行緒執行完畢(lock.unlock())時會啟用自己的後繼節點,但正在執行的執行緒並不在佇列中,而那些等待執行的執行緒全 部處於阻塞狀態,經過調查執行緒的顯式阻塞是通過呼叫LockSupport.park()完成,而LockSupport.park()則呼叫 sun.misc.Unsafe.park()本地方法,再進一步,HotSpot在Linux中中通過呼叫pthread_mutex_lock函式把 執行緒交給系統核心進行阻塞。

    與synchronized相同的是,這也是一個虛擬佇列,不存在佇列例項,僅存在節點之間的前後關係。令人疑惑的是為什麼採用CLH佇列呢?原生的CLH佇列是用於自旋鎖,但Doug Lea把其改造為阻塞鎖

    當有執行緒競爭鎖時,該執行緒會首先嚐試獲得鎖,這對於那些已經在佇列中排隊的執行緒來說顯得不公平,這也是非公平鎖的由來,與synchronized實現類似,這樣會極大提高吞吐量。 如果已經存在Running執行緒,則新的競爭執行緒會被追加到隊尾,具體是採用基於CAS的Lock-Free演算法,因為執行緒併發對Tail呼叫CAS可能會 導致其他執行緒CAS失敗,解決辦法是迴圈CAS直至成功。AQS的實現非常精巧,令人歎為觀止,不入細節難以完全領會其精髓,下面詳細說明實現過程:

    AbstractQueuedSynchronizer通過構造一個基於阻塞的CLH佇列容納所有的阻塞執行緒,而對該佇列的操作均通過Lock-Free(CAS)操作,但對已經獲得鎖的執行緒而言,ReentrantLock實現了偏向鎖的功能。

synchronized 的底層也是一個基於CAS操作的等待佇列,但JVM實現的更精細,把等待佇列分為ContentionList和EntryList,目的是為了降低執行緒的出列速度;當然也實現了偏向鎖,從資料結構來說二者設計沒有本質區別。但synchronized還實現了自旋鎖,並針對不同的系統和硬體體系進行了優 化,而Lock則完全依靠系統阻塞掛起等待執行緒。

當然Lock比synchronized更適合在應用層擴充套件,可以繼承 AbstractQueuedSynchronizer定義各種實現,比如實現讀寫鎖(ReadWriteLock),公平或不公平鎖;同時,Lock對 應的Condition也比wait/notify要方便的多、靈活的多。

 

state值,若為0,意味著此時沒有執行緒獲取到資源

簡述總結:

    總體來講執行緒獲取鎖要經歷以下過程(非公平):

    1、呼叫lock方法,會先進行cas操作看下可否設定同步狀態1成功,如果成功執行臨界區程式碼

    2、如果不成功獲取同步狀態,如果狀態是0那麼cas設定為1.

    3、如果同步狀態既不是0也不是自身執行緒持有會把當前執行緒構造成一個節點。

    4、把當前執行緒節點CAS的方式放入佇列中,行為上執行緒阻塞,內部自旋獲取狀態。

    (acquireQueued的主要作用是把已經追加到佇列的執行緒節點進行阻塞,但阻塞前又通過tryAccquire重試是否能獲得鎖,如果重試成功能則無需阻塞,直接返回。)

    5、執行緒釋放鎖,喚醒佇列第一個節點,參與競爭。重複上述。

 


synchronized和lock的底層區別*

synchronized的底層也是一個基於CAS操作的等待佇列,但JVM實現的更精細,把等待佇列分為ContentionList和EntryList,目的是為了降低執行緒的出列速度;當然也實現了偏向鎖,從資料結構來說二者設計沒有本質區別。但synchronized還實現了自旋鎖,並針對不同的系統和硬體體系進行了優化,而Lock則完全依靠系統阻塞掛起等待執行緒。**

 

當然Lock比synchronized更適合在應用層擴充套件,可以繼承AbstractQueuedSynchronizer定義各種實現,比如實現讀寫鎖(ReadWriteLock),公平或不公平鎖;同時,Lock對應的Condition也比wait/notify要方便的多、靈活的多。

ReentrantLock是一個可重入的互斥鎖,ReentrantLock由最近成功獲取鎖,還沒有釋放的執行緒所擁有

ReentrantLock與synchronized的區別

--ReentrantLock的lock機制有2種,忽略中斷鎖和響應中斷鎖

 

--synchronized實現的鎖機制是可重入的,主要區別是中斷控制和競爭鎖公平策略


兩者區別:

1.首先synchronized是java內建關鍵字,在jvm層面,Lock是個java類;

2.synchronized無法判斷是否獲取鎖的狀態,Lock可以判斷是否獲取到鎖;

3.synchronized會自動釋放鎖(a 執行緒執行完同步程式碼會釋放鎖 ;b 執行緒執行過程中發生異常會釋放鎖),Lock需在finally中手工釋放鎖(unlock()方法釋放鎖),否則容易造成執行緒死鎖;

4.用synchronized關鍵字的兩個執行緒1和執行緒2,如果當前執行緒1獲得鎖,執行緒2執行緒等待。如果執行緒1阻塞,執行緒2則會一直等待下去,而Lock鎖就不一定會等待下去,如果嘗試獲取不到鎖,執行緒可以不用一直等待就結束了;

5.synchronized的鎖可重入、不可中斷、非公平,而Lock鎖可重入、可判斷、可公平(兩者皆可)

6.Lock鎖適合大量同步的程式碼的同步問題,synchronized鎖適合程式碼少量的同步問題。

 

synchronized底層實現

synchronized 屬於重量級鎖,效率低下,因為監視器鎖(monitor)是依賴於底層作業系統的 Mutex Lock 來實現的,而作業系統實現執行緒之間的切換時需要從使用者態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是為什麼早期的 synchronized 效率低的原因。慶幸的是在 Java 6 之後 Java 官方從 JVM 層面對 synchronized 進行了較大優化,所以現在的 synchronized 鎖效率也優化得很不錯了。Java 6 之後,為了減少獲得鎖和釋放鎖所帶來的效能消耗,引入了輕量級鎖和偏向鎖,

 

Lock底層實現

Lock底層實現基於AQS實現,採用執行緒獨佔的方式,在硬體層面依賴特殊的CPU指令(CAS)。

簡單來說,ReenTrantLock的實現是一種自旋鎖,通過迴圈呼叫CAS操作來實現加鎖。它的效能比較好也是因為避免了使執行緒進入核心態的阻塞狀態。想盡辦法避免執行緒進入核心的阻塞狀態是我們去分析和理解鎖設計的關鍵鑰匙。

 

volatile底層實現

在JVM底層volatile是採用“記憶體屏障”來實現的。

 

lock和Monitor的區別

一、lock的底層本身是Monitor來實現的,所以Monitor可以實現lock的所有功能。

二、Monitor有TryEnter的功能,可以防止出現死鎖的問題,lock沒有。