Java中可重入鎖ReentrantLock原理剖析

2021-02-20 java一日一條
一、 概述

本文首先介紹Lock接口、ReentrantLock的類層次結構以及鎖功能模板類AbstractQueuedSynchronizer的簡單原理,然後通過分析ReentrantLock的lock方法和unlock方法,來解釋ReentrantLock的內部原理,最後做一個總結。本文不涉及ReentrantLock中的條件變量。

1.1、Lock接口

Lock接口,是對控制並發的工具的抽象。它比使用synchronized關鍵詞更靈活,並且能夠支持條件變量。它是一種控制並發的工具,一般來說,它控制對某種共享資源的獨佔。也就是說,同一時間內只有一個線程可以獲取這個鎖並佔用資源。其他線程想要獲取鎖,必須等待這個線程釋放鎖。在Java實現中的ReentrantLock就是這樣的鎖。另外一種鎖,它可以允許多個線程讀取資源,但是只能允許一個線程寫入資源,ReadWriteLock就是這樣一種特殊的鎖,簡稱讀寫鎖。下面是對Lock接口的幾個方法的總體描述:



接下來,我們將圍繞lock和unlock這兩個方法,來介紹整個ReentrantLock是怎麼工作的。在介紹ReentrantLock之前,我們首先來看一下ReentrantLock的類層次結構以及和它密切相關的AbstractQueuedSynchronizer

1.2、ReentrantLock類層次結構

ReentrantLock實現了Lock接口,內部有三個內部類,Sync、NonfairSync、FairSync,Sync是一個抽象類型,它繼承AbstractQueuedSynchronizer,這個AbstractQueuedSynchronizer是一個模板類,它實現了許多和鎖相關的功能,並提供了鉤子方法供用戶實現,比如tryAcquire,tryRelease等。Sync實現了AbstractQueuedSynchronizer的tryRelease方法。NonfairSync和FairSync兩個類繼承自Sync,實現了lock方法,然後分別公平搶佔和非公平搶佔針對tryAcquire有不同的實現。

1.3、AbstractQueuedSynchronizer

首先,AbstractQueuedSynchronizer繼承自AbstractOwnableSynchronizer,AbstractOwnableSynchronizer的實現很簡單,它表示獨佔的同步器,內部使用變量exclusiveOwnerThread表示獨佔的線程。

其次,AbstractQueuedSynchronizer內部使用CLH鎖隊列來將並發執行變成串行執行。整個隊列是一個雙向鍊表。每個CLH鎖隊列的節點,會保存前一個節點和後一個節點的引用,當前節點對應的線程,以及一個狀態。這個狀態用來表明該線程是否應該block。當節點的前一個節點被釋放的時候,當前節點就被喚醒,成為頭部。新加入的節點會放在隊列尾部。

二、 非公平鎖的lock方法2.1、lock方法流程圖

2.2、lock方法詳細描述

1、在初始化ReentrantLock的時候,如果我們不傳參數是否公平,那麼默認使用非公平鎖,也就是NonfairSync。

2、當我們調用ReentrantLock的lock方法的時候,實際上是調用了NonfairSync的lock方法,這個方法先用CAS操作,去嘗試搶佔該鎖。如果成功,就把當前線程設置在這個鎖上,表示搶佔成功。如果失敗,則調用acquire模板方法,等待搶佔。代碼如下:


3、調用acquire(1)實際上使用的是AbstractQueuedSynchronizer的acquire方法,它是一套鎖搶佔的模板,總體原理是先去嘗試獲取鎖,如果沒有獲取成功,就在CLH隊列中增加一個當前線程的節點,表示等待搶佔。然後進入CLH隊列的搶佔模式,進入的時候也會去執行一次獲取鎖的操作,如果還是獲取不到,就調用LockSupport.park將當前線程掛起。那麼當前線程什麼時候會被喚醒呢?當持有鎖的那個線程調用unlock的時候,會將CLH隊列的頭節點的下一個節點上的線程喚醒,調用的是LockSupport.unpark方法。acquire代碼比較簡單,具體如下:
3.1、acquire方法內部先使用tryAcquire這個鉤子方法去嘗試再次獲取鎖,這個方法在NonfairSync這個類中其實就是使用了nonfairTryAcquire,具體實現原理是先比較當前鎖的狀態是否是0,如果是0,則嘗試去原子搶佔這個鎖(設置狀態為1,然後把當前線程設置成獨佔線程),如果當前鎖的狀態不是0,就去比較當前線程和佔用鎖的線程是不是一個線程,如果是,會去增加狀態變量的值,從這裡看出可重入鎖之所以可重入,就是同一個線程可以反覆使用它佔用的鎖。如果以上兩種情況都不通過,則返回失敗false。代碼如下:

3.2、tryAcquire一旦返回false,就會則進入acquireQueued流程,也就是基於CLH隊列的搶佔模式:

3.2.1、首先,在CLH鎖隊列尾部增加一個等待節點,這個節點保存了當前線程,通過調用addWaiter實現,這裡需要考慮初始化的情況,在第一個等待節點進入的時候,需要初始化一個頭節點然後把當前節點加入到尾部,後續則直接在尾部加入節點就行了。

代碼如下:


3.2.2、將節點增加到CLH隊列後,進入acquireQueued方法。

首先,外層是一個無限for循環,如果當前節點是頭節點的下個節點,並且通過tryAcquire獲取到了鎖,說明頭節點已經釋放了鎖,當前線程是被頭節點那個線程喚醒的,這時候就可以將當前節點設置成頭節點,並且將failed標記設置成false,然後返回。至於上一個節點,它的next變量被設置為null,在下次GC的時候會清理掉。

如果本次循環沒有獲取到鎖,就進入線程掛起階段,也就是shouldParkAfterFailedAcquire這個方法。

代碼如下:


3.2.3、如果嘗試獲取鎖失敗,就會進入shouldParkAfterFailedAcquire方法,會判斷當前線程是否掛起,如果前一個節點已經是SIGNAL狀態,則當前線程需要掛起。如果前一個節點是取消狀態,則需要將取消節點從隊列移除。如果前一個節點狀態是其他狀態,則嘗試設置成SIGNAL狀態,並返回不需要掛起,從而進行第二次搶佔。完成上面的事後進入掛起階段。

代碼如下:


3.2.4、當進入掛起階段,會進入parkAndCheckInterrupt方法,則會調用LockSupport.park(this)將當前線程掛起。代碼:

三、 非公平鎖的unlock方法3.1、unlock方法的活動圖

3.2、unlock方法詳細描述

1、調用unlock方法,其實是直接調用AbstractQueuedSynchronizer的release操作。

2、進入release方法,內部先嘗試tryRelease操作,主要是去除鎖的獨佔線程,然後將狀態減一,這裡減一主要是考慮到可重入鎖可能自身會多次佔用鎖,只有當狀態變成0,才表示完全釋放了鎖。

3、一旦tryRelease成功,則將CHL隊列的頭節點的狀態設置為0,然後喚醒下一個非取消的節點線程。

4、一旦下一個節點的線程被喚醒,被喚醒的線程就會進入acquireQueued代碼流程中,去獲取鎖。

具體代碼如下:

unlock代碼:


release方法代碼:
Sync中通用的tryRelease方法代碼:
unparkSuccessor代碼:

四、 公平鎖和非公平鎖的區別

公平鎖和非公平鎖,在CHL隊列搶佔模式上都是一致的,也就是在進入acquireQueued這個方法之後都一樣,它們的區別在初次搶佔上有區別,也就是tryAcquire上的區別,下面是兩者內部調用關係的簡圖:


真正的區別就是公平鎖多了hasQueuePredecessors這個方法,這個方法用於判斷CHL隊列中是否有節點,對於公平鎖,如果CHL隊列有節點,則新進入競爭的線程一定要在CHL上排隊,而非公平鎖則是無視CHL隊列中的節點,直接進行競爭搶佔,這就有可能導致CHL隊列上的節點永遠獲取不到鎖,這就是非公平鎖之所以不公平的原因。

五、 總結

線程使用ReentrantLock獲取鎖分為兩個階段,第一個階段是初次競爭,第二個階段是基於CHL隊列的競爭。在初次競爭的時候是否考慮隊列節點直接區分出了公平鎖和非公平鎖。在基於CHL隊列的鎖競爭中,依靠CAS操作保證原子操作,依靠LockSupport來做線程的掛起和喚醒,使用隊列來保證並發執行變成了串行執行,從而消除了並發所帶來的問題。總體來說,ReentrantLock是一個比較輕量級的鎖,而且使用面向對象的思想去實現了鎖的功能,比原來的synchronized關鍵字更加好理解。

相關焦點

  • 徹底理解ReentrantLock可重入鎖的使用
    java除了使用關鍵字synchronized外,還可以使用ReentrantLock實現獨佔鎖的功能。而且ReentrantLock相比synchronized而言功能更加豐富,使用起來更為靈活,也更適合複雜的並發場景。這篇文章主要是從使用的角度來分析一下ReentrantLock。
  • JAVA並發編程:concurrent提供了哪些比synchronized更高效的鎖
    在java.util.locks包下面提供了三種鎖,ReentrantLock、ReentrantReadWriteLock和StampedLock,下面一一講解這三種鎖的特點、實現方式以及應用。tryLock(long time, TimeUnit unit) throws InterruptedException;嘗試獲取鎖,直到超時;void unlock();解鎖操作newCondition()方法;實現等待、通知模式,後面例子中會講解其應用ReentrantLockreentrantLock可以實現synchronized的所有功能
  • Java 中15種鎖的介紹:公平鎖,可重入鎖,獨享鎖,互斥鎖,樂觀鎖,分段鎖,自旋鎖等等
    可重入鎖 / 不可重入鎖可重入鎖廣義上的可重入鎖指的是可重複可遞歸調用的鎖,在外層使用鎖之後,在內層仍然可以使用,並且不發生死鎖(前提得是同一個對象或者class),這樣的鎖就叫做可重入鎖。);}上面的代碼就是一個可重入鎖的一個特點,如果不是可重入鎖的話,setB可能不會被當前線程執行,可能造成死鎖。
  • 使用Lock鎖:java多線程安全問題解決方案之Lock鎖
    今天我們來學習一下Lock鎖,它是java 1.5之後出現的接口 java.util.concurrent.locks.Lock接口Lock實現提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作,就來釋放鎖和獲取鎖來說,在使用synchronized 方法的時候,我們並不知道什麼時候獲取到了鎖,什麼時候釋放了鎖,而Lock接口不一樣,他提供了專門的獲取鎖和釋放鎖的方法。
  • ReentrantLock核心原理,絕對乾貨
    不知道也沒關係,看完這篇文章通過你的思考,能找到答案哦那我們開始吧ReentrantLock 中文我們叫做可重入互斥鎖,可重入的意思是同一個線程可以對同一個共享資源重複的加鎖或釋放鎖,互斥就是 AQS 中的排它鎖的意思,只允許一個線程獲得鎖
  • Java面試的的時候你被提過哪些問題?
    20. java多態的實現原理。21. 實現多線程的兩種方法:Thread與Runable。22. 線程同步的方法:sychronized、lock、reentrantLock等。23. 鎖的等級:方法鎖、對象鎖、類鎖。24. 寫出生產者消費者模式。25. ThreadLocal的設計理念與作用。
  • 深入理解ReentrantLock的實現原理
    非公平鎖的實現原理當我們使用無參構造方法構造的時候即ReentrantLock lock = new ReentrantLock(),創建的就是非公平鎖。判斷此次釋放鎖後state的值是否為0,如果是則代表鎖有沒有重入,然後將鎖的所有者設置成null且返回true,然後執行步驟3,如果不是則代表鎖發生了重入執行步驟4。現在鎖已經釋放完,即state=0,喚醒同步隊列中的後繼節點進行鎖的獲取。鎖還沒有釋放完,即state!
  • 24張圖帶你徹底理解Java中的21種鎖
    可重入鎖synchronized、Reentrantlock、Lock5讀寫鎖ReentrantReadWriteLock,CopyOnWriteArrayList、CopyOnWriteArraySet6公平鎖Reentrantlock(true)7非公平鎖synchronized、reentrantlock(false)8共享鎖ReentrantReadWriteLock中讀鎖9獨佔鎖synchronized
  • 這篇 ReentrantLock 你可以看得懂!
    初識 ReentrantLockReentrantLock 位於 java.util.concurrent.locks 包下,它實現了 Lock 接口和 Serializable 接口。ReentrantLock 是一把可重入鎖和互斥鎖,它具有與 synchronized 關鍵字相同的含有隱式監視器鎖(monitor)的基本行為和語義,但是它比 synchronized
  • ReentrantLock 實現原理深入探究
    網上寫ReentrantLock的使用、ReentrantLock和synchronized的區別的文章很多,研究ReentrantLock並且能講清楚ReentrantLock的原理的文章很少,本文就來研究一下ReentrantLock的實現原理。研究ReentrantLock的實現原理需要比較好的Java基礎以及閱讀代碼的能力,有些朋友看不懂沒關係,可以以後看,相信你一定會有所收穫。
  • Java中的讀寫鎖ReentrantReadWriteLock
    Java中的讀寫鎖ReentrantReadWriteLockReentrantLock和synchronized都屬於獨佔鎖,每次同一時刻都只能有一個線程訪問,導致並發不高。公平性,重入性在重入鎖的時候已經提到,重點說下鎖降級。鎖降級:一個線程獲取了寫鎖,在釋放前有獲取了讀鎖,然後釋放寫鎖,此時寫鎖降級為讀鎖。
  • Java並發包下鎖學習第一篇:介紹及學習安排
    Java並發包下鎖學習第一篇:介紹及學習安排在Java並發編程中,實現鎖的方式有兩種,分別是:可以使用同步鎖(synchronized關鍵字的鎖),還有lock接口下的鎖。在這個系列中,我們將會學習並發包下鎖實現的原理(我們將跟著源碼來分析)、什麼是可重入鎖、公平鎖和非公平鎖怎麼定義的、為什麼synchronized關鍵字的鎖和ReentrantLock默認會選擇非公平鎖?讀寫鎖和獨佔鎖的比較、跟著源碼我們來分析讀寫鎖等和鎖相關的知識。學完這個系列教程後,大家將對並發鎖有更新的理解,歡迎大家一起學習。
  • 面試官:你說說ReentrantLock和Synchronized區別
    可重入鎖Synchronized和ReentrantLock都是可重入的,Synchronized是本地方法是C++實現,而ReentrantLock是JUC包用Java實現用一個形象例子來說明:如下圖:一個房中房,房裡外各有一把鎖,但只有唯一的鑰匙可以開,擁有鑰匙的人可以先進入門1,再進入門2,其中進入門2就是叫鎖可重入了。
  • 「原創」Java並發編程系列09|基礎乾貨
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第9篇。現在,我們進入正題:介紹並發編程的基礎性概念。
  • 面經手冊 · 第16篇《碼農會鎖,ReentrantLock之公平鎖講解和實現》
    ReentrantLock 知識鏈條圖 16-1 ReentrantLock 鎖知識鏈條ReentrantLock 是基於 Lock 實現的可重入鎖,所有的 Lock 都是基於 AQS 實現的,AQS 和 Condition 各自維護不同的對象,在使用 Lock 和 Condition 時,其實就是兩個隊列的互相移動。
  • Java之戳中痛點之 synchronized 深度解析|java|override|author|...
    、不可中斷    原理:加解鎖原理、可重入原理、可見性原理    缺陷:效率低、不夠靈活、無法預判是否成功獲取到鎖    如何選擇Lock或Synchronized    如何提高性能、JVM如何決定哪個線程獲取鎖    總結
  • 可重入讀寫鎖ReentrantReadWriteLock的使用詳解
    ReentrantReadWriteLock是一把可重入讀寫鎖,這篇文章主要是從使用的角度幫你理解,希望對你有幫助。一、性質1、可重入如果你了解過synchronized關鍵字,一定知道他的可重入性,可重入就是同一個線程可以重複加鎖,每次加鎖的時候count值加1,每次釋放鎖的時候count減1,直到count為0,其他的線程才可以再次獲取。
  • 「原創」Java並發編程系列28|Copy-On-Write容器
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫正文前面兩篇講了並發編程中線程安全HashMap:ConcurrentHashMap
  • 深入分析synchronized底層加鎖的原理
    synchronized是可重入鎖,就是可以多次加鎖,每加一次,monitor裡加鎖計數器都加1。2、在離開加鎖代碼塊是增加一個monitorexit指令,然後遞減monitor裡的加鎖計數器,如果加鎖計數器遞減為0,就標誌當前線程不持有鎖,也就是釋放了鎖。