ReentrantLock 的實現原理

AQS的功能可以分爲獨佔和共享,ReentrantLock實現了獨佔功能。

ReentrantLock實現了Lock接口,加鎖和解鎖都需要顯式寫出,注意一定要在適當時候unlock。

 

 

ReentrantLock對比synchronized

和synchronized相比,ReentrantLock用起來會複雜一些。在基本的加鎖和解鎖上,兩者是一樣的,所以無特殊情況下,推薦使用synchronized。ReentrantLock的優勢在於它更靈活、更強大,增加了輪訓、超時、中斷等高級功能。

 

 

公平鎖和非公平鎖

 

ReentrantLock的內部類Sync繼承了AQS,分爲公平鎖FairSync和非公平鎖NonfairSync。

  • 公平鎖:線程獲取鎖的順序和調用lock的順序一樣,FIFO;
  • 非公平鎖:線程獲取鎖的順序和調用lock的順序無關,全憑運氣。

ReentrantLock默認使用非公平鎖是基於性能考慮,公平鎖爲了保證線程規規矩矩地排隊,需要增加阻塞和喚醒的時間開銷。如果直接插隊獲取非公平鎖,跳過了對隊列的處理,速度會更快。

 

嘗試獲取鎖

 

 

獲取鎖成功分爲兩種情況,第一個if判斷AQS的state是否等於0,表示鎖沒有人佔有。接着,hasQueuedPredecessors判斷隊列是否有排在前面的線程在等待鎖,沒有的話調用compareAndSetState使用cas的方式修改state,傳入的acquires寫死是1。最後線程獲取鎖成功,setExclusiveOwnerThread將線程記錄爲獨佔鎖的線程。

第二個if判斷當前線程是否爲獨佔鎖的線程,因爲ReentrantLock是可重入的,線程可以不停地lock來增加state的值,對應地需要unlock來解鎖,直到state爲零。

如果最後獲取鎖失敗,下一步需要將線程加入到等待隊列。

 

線程進入等待隊列

AQS內部有一條雙向的隊列存放等待線程,節點是Node對象。每個Node維護了線程、前後Node的指針和等待狀態等參數。

線程在加入隊列之前,需要包裝進Node,調用方法是addWaiter

 

 

 

每個Node需要標記是獨佔的還是共享的,由傳入的mode決定,ReentrantLock自然是使用獨佔模式Node.EXCLUSIVE。

創建好Node後,如果隊列不爲空,使用cas的方式將Node加入到隊列尾。注意,這裏只執行了一次修改操作,並且可能因爲併發的原因失敗。因此修改失敗的情況和隊列爲空的情況,需要進入enq。

 

 

釋放鎖

通過上面詳細的獲取鎖過程分析,釋放鎖過程大概可以猜到:頭節點是獲取鎖的線程,先移出隊列,再通知後面的節點獲取鎖。

ReentrantLock的unlock方法很簡單地調用了AQS的release:

和lock的tryAcquire一樣,unlock的tryRelease同樣由ReentrantLock實現:

 

因爲鎖是可以重入的,所以每次lock會讓state加1,對應地每次unlock要讓state減1,直到爲0時將獨佔線程變量設置爲空,返回標記是否徹底釋放鎖。

最後,調用unparkSuccessor將頭節點的下個節點喚醒:

 

 

 

尋找下個待喚醒的線程是從隊列尾向前查詢的,找到線程後調用LockSupport的unpark方法喚醒線程。被喚醒的線程重新執行acquireQueued裏的循環,就是上文關於acquireQueued標記1部分,線程重新嘗試獲取鎖。

 

 

非公平鎖

分析完公平鎖的實現,還剩下非公平鎖,主要區別是獲取鎖的過程不同。

在NonfairSync的lock方法裏,第一步直接嘗試將state修改爲1,很明顯,這是搶先獲取鎖的過程。如果修改state失敗,則和公平鎖一樣,調用acquire。

 

nonfairTryAcquire和tryAcquire乍一看幾乎一樣,差異只是缺少調用hasQueuedPredecessors。這點體驗出公平鎖和非公平鎖的不同,公平鎖會關注隊列裏排隊的情況,老老實實按照FIFO的次序;非公平鎖只要有機會就搶佔,纔不管排隊的事。

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