可重入鎖的機制



1、聽故事把知識掌握了

在一個村子里面,有一口井水,水質非常的好,村民們都想打井里的水。這井只有一口,村里的人那么多,所以得出個打水的規則才行。村長絞盡腦汁,最終想出了一個比較合理的方案,咱們來仔細的看看聰明的村長大人的智慧。

井邊安排一個看井人,維護打水的秩序。

打水時,以家庭為單位,哪個家庭任何人先到井邊,就可以先打水,而且如果一個家庭占到了打水權,其家人這時候過來打水不用排隊。而那些沒有搶占到打水權的人,一個一個挨着在井邊排成一隊,先到的排在前面。打水示意圖如下 :

打水示意圖

是不是感覺很和諧,如果打水的人打完了,他會跟看井人報告,看井人會讓第二個人接着打水。這樣大家總都能夠打到水。是不是看起來挺公平的,先到的人先打水,當然不是絕對公平的,自己看看下面這個場景 :

同家人一起打水

看着,一個有娃的父親正在打水,他的娃也到井邊了,所以女憑父貴直接排到最前面打水,羡煞旁人了。 
以上這個故事模型就是所謂的公平鎖模型,當一個人想到井邊打水,而現在打水的人又不是自家人,這時候就得乖乖在隊列后面排隊。

事情總不是那么一帆風順的,總會有些人想走捷徑,話說看井人年紀大了,有時候,眼力不是很好,這時候,人們開始打起了新主意。新來打水的人,他們看到有人排隊打水的時候,他們不會那么乖巧的就排到最后面去排隊,反之,他們會看看現在有沒有人正在打水,如果有人在打水,沒輒了,只好排到隊列最后面,但如果這時候前面打水的人剛剛打完水,正在交接中,排在隊頭的人還沒有完成交接工作,這時候,新來的人可以嘗試搶打水權,如果搶到了,呵呵,其他人也只能睜一只眼閉一只眼,因為大家都默認這個規則了。這就是所謂的非公平鎖模型。新來的人不一定總得乖乖排隊,這也就造成了原來隊列中排隊的人可能要等很久很久。 

java可重入鎖-ReentrantLock實現細節

ReentrantLock支持兩種獲取鎖的方式,一種是公平模型,一種是非公平模型。在繼續之前,咱們先把故事元素轉換為程序元素。

元素轉換 

咱們先來說說公平鎖模型:

初始化時, state=0,表示無人搶占了打水權。這時候,村民A來打水(A線程請求鎖),占了打水權,把state+1,如下所示:

線程A獲取鎖

線程A取得了鎖,把 state原子性+1,這時候state被改為1,A線程繼續執行其他任務,然后來了村民B也想打水(線程B請求鎖),線程B無法獲取鎖,生成節點進行排隊,如下圖所示:

線程B等待

初始化的時候,會生成一個空的頭節點,然后才是B線程節點,這時候,如果線程A又請求鎖,是否需要排隊?答案當然是否定的,否則就直接死鎖了。當A再次請求鎖,就相當於是打水期間,同一家人也來打水了,是有特權的,這時候的狀態如下圖所示:

可重入鎖獲取

 

此處可能有人會問 在代碼里邊怎么理解這種可重入鎖的形態呢?

復制代碼
    public static ReentrantLock lock = new ReentrantLock(); public static int i = 0; public void run()   { for (int j = 0;j<100000;j++)      { lock.lock(); lock.lock(); try { i++; }finally { lock.unlock(); lock.unlock(); } } }
復制代碼

 

為什么需要使用可重入鎖 在故事描述完后進行具體說明;

 

到了這里,相信大家應該明白了什么是可重入鎖了吧。就是一個線程在獲取了鎖之后,再次去獲取了同一個鎖,這時候僅僅是把狀態值進行累加。如果線程A釋放了一次鎖,就成這樣了:

線程A釋放一次鎖

僅僅是把狀態值減了,只有線程A把此鎖全部釋放了,狀態值減到0了,其他線程才有機會獲取鎖。當A把鎖完全釋放后,state恢復為0,然后會通知隊列喚醒B線程節點,使B可以再次競爭鎖。當然,如果B線程后面還有C線程,C線程繼續休眠,除非B執行完了,通知了C線程。注意,當一個線程節點被喚醒然后取得了鎖,對應節點會從隊列中刪除。 

非公平鎖模型

如果你已經明白了前面講的公平鎖模型,那么非公平鎖模型也就非常容易理解了。當線程A執行完之后,要喚醒線程B是需要時間的,而且線程B醒來后還要再次競爭鎖,所以如果在切換過程當中,來了一個線程C,那么線程C是有可能獲取到鎖的,如果C獲取到了鎖,B就只能繼續乖乖休眠了。這里就不再畫圖說明了。 

其它知識點

java5中添加了一個並發包, java.util.concurrent,里面提供了各種並發的工具類,通過此工具包,可以在java當中實現功能非常強大的多線程並發操作。對於每個java攻城獅,我覺得非常有必要了解這個包的功能。雖然做不到一步到位,但慢慢虛心學習,沉下心來,總能慢慢領悟到java多線程編程的精華。 

本問故事情節轉載自其他博客,原文地址:https://blog.csdn.net/yanyan19880509/article/details/52345422/

 

2、為什么使用可重入鎖?

  ReentrantLock 是一個可重入的互斥(/獨占)鎖,又稱為“獨占鎖”。

ReentrantLock通過自定義隊列同步器(AQS-AbstractQueuedSychronized,是實現鎖的關鍵)來實現鎖的獲取與釋放。

其可以完全替代 synchronized 關鍵字。JDK 5.0 早期版本,其性能遠好於 synchronized,但 JDK 6.0 開始,JDK 對 synchronized 做了大量的優化,使得兩者差距並不大。

“獨占”,就是在同一時刻只能有一個線程獲取到鎖,而其它獲取鎖的線程只能處於同步隊列中等待,只有獲取鎖的線程釋放了鎖,后繼的線程才能夠獲取鎖。

“可重入”,就是支持重進入的鎖,它表示該鎖能夠支持一個線程對資源的重復加鎖。

該鎖還支持獲取鎖時的公平和非公平性選擇。“公平”是指“不同的線程獲取鎖的機制是公平的”,而“不公平”是指“不同的線程獲取鎖的機制是非公平的”。

2、1 中斷響應(lockInterruptibly)

  對於 synchronized 來說,如果一個線程在等待鎖,那么結果只有兩種情況,獲得這把鎖繼續執行,或者線程就保持等待。

而使用重入鎖,提供了另一種可能,這就是線程可以被中斷。也就是在等待鎖的過程中,程序可以根據需要取消對鎖的需求。

下面的例子中,產生了死鎖,但得益於鎖中斷,最終解決了這個死鎖:

復制代碼
public class IntLock implements Runnable{ public static ReentrantLock lock1 = new ReentrantLock(); public static ReentrantLock lock2 = new ReentrantLock(); int lock; /** * 控制加鎖順序,產生死鎖 */ public IntLock(int lock) { this.lock = lock; } public void run() { try { if (lock == 1) { lock1.lockInterruptibly(); // 如果當前線程未被 中斷,則獲取鎖。 try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } lock2.lockInterruptibly(); System.out.println(Thread.currentThread().getName()+",執行完畢!"); } else { lock2.lockInterruptibly(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } lock1.lockInterruptibly(); System.out.println(Thread.currentThread().getName()+",執行完畢!"); } } catch (InterruptedException e) { e.printStackTrace(); } finally { // 查詢當前線程是否保持此鎖。 if (lock1.isHeldByCurrentThread()) { lock1.unlock(); } if (lock2.isHeldByCurrentThread()) { lock2.unlock(); } System.out.println(Thread.currentThread().getName() + ",退出。"); } } public static void main(String[] args) throws InterruptedException { IntLock intLock1 = new IntLock(1); IntLock intLock2 = new IntLock(2); Thread thread1 = new Thread(intLock1, "線程1"); Thread thread2 = new Thread(intLock2, "線程2"); thread1.start(); thread2.start(); Thread.sleep(1000); thread2.interrupt(); // 中斷線程2  } }
復制代碼

上述例子中,線程 thread1 和 thread2 啟動后,thread1 先占用 lock1,再占用 lock2;thread2 反之,先占 lock2,后占 lock1。這便形成 thread1 和 thread2 之間的相互等待。

代碼 56 行,main 線程處於休眠(sleep)狀態,兩線程此時處於死鎖的狀態,代碼 57 行 thread2 被中斷(interrupt),故 thread2 會放棄對 lock1 的申請,同時釋放已獲得的 lock2。這個操作導致 thread1 順利獲得 lock2,從而繼續執行下去。

執行代碼,輸出如下:

 

2、2鎖申請等待限時(tryLock)

  除了等待外部通知(中斷操作 interrupt )之外,限時等待也可以做到避免死鎖。

  通常,無法判斷為什么一個線程遲遲拿不到鎖。也許是因為產生了死鎖,也許是產生了飢餓。但如果給定一個等待時間,讓線程自動放棄,那么對系統來說是有意義的。可以使用 tryLock() 方法進行一次限時的等待。

 

復制代碼
public class TimeLock implements Runnable{ public static ReentrantLock lock = new ReentrantLock(); public void run() { try { if (lock.tryLock(5, TimeUnit.SECONDS)) { Thread.sleep(6 * 1000); }else { System.out.println(Thread.currentThread().getName()+" get Lock Failed"); } } catch (InterruptedException e) { e.printStackTrace(); }finally { // 查詢當前線程是否保持此鎖。 if (lock.isHeldByCurrentThread()) { System.out.println(Thread.currentThread().getName()+" release lock"); lock.unlock(); } } } /** * 在本例中,由於占用鎖的線程會持有鎖長達6秒,故另一個線程無法再5秒的等待時間內獲得鎖,因此請求鎖會失敗。 */ public static void main(String[] args) { TimeLock timeLock = new TimeLock(); Thread t1 = new Thread(timeLock, "線程1"); Thread t2 = new Thread(timeLock, "線程2"); t1.start(); t2.start(); } }
復制代碼

  上述例子中,由於占用鎖的線程會持有鎖長達 6 秒,故另一個線程無法在 5 秒的等待時間內獲得鎖,因此,請求鎖失敗。

  ReentrantLock.tryLock()方法也可以不帶參數直接運行。這種情況下,當前線程會嘗試獲得鎖,如果鎖並未被其他線程占用,則申請鎖成功,立即返回 true。否則,申請失敗,立即返回 false,當前線程不會進行等待。這種模式不會引起線程等待,因此也不會產生死鎖。

2、3 公平鎖

  ·默認情況下,鎖的申請都是非公平的。也就是說,如果線程 1 與線程 2,都申請獲得鎖 A,那么誰獲得鎖不是一定的,是由系統在等待隊列中隨機挑選的。這就好比,買票的人不排隊,售票姐姐只能隨機挑一個人賣給他,這顯然是不公平的。而公平鎖,它會按照時間的先后順序,保證先到先得。公平鎖的特點是:不會產生飢餓現象。

  重入鎖允許對其公平性進行設置。構造函數如下:

public ReentrantLock(boolean fair)
復制代碼
public class FairLock implements Runnable{ public static ReentrantLock fairLock = new ReentrantLock(true); public void run() { while (true) { try { fairLock.lock(); System.out.println(Thread.currentThread().getName()+",獲得鎖!"); }finally { fairLock.unlock(); } } } public static void main(String[] args) { FairLock fairLock = new FairLock(); Thread t1 = new Thread(fairLock, "線程1"); Thread t2 = new Thread(fairLock, "線程2"); t1.start();t2.start(); } }
復制代碼

測試結果:

  1.當參數設置為 true 時:線程1 和 線程2 交替進行 公平競爭 交替打印

復制代碼
線程1,獲得鎖!
線程2,獲得鎖! 線程1,獲得鎖! 線程2,獲得鎖! 線程1,獲得鎖! 線程2,獲得鎖! 線程1,獲得鎖! 線程2,獲得鎖! 線程1,獲得鎖! 線程2,獲得鎖! 線程1,獲得鎖! 線程2,獲得鎖! 線程1,獲得鎖! 線程2,獲得鎖! 線程1,獲得鎖!
復制代碼

 

  2.當參數設置為 false 時: 此時可以看到線程1 可以持續拿到鎖 等線程1 執行完后 線程2 才可以拿到線程 然后多次執行 ; 這就是使用 可重入鎖后 是非公平機制 線程可以優先多次拿到執行權

復制代碼
線程1,獲得鎖!
線程1,獲得鎖!
線程1,獲得鎖!
線程1,獲得鎖!
線程1,獲得鎖!
線程1,獲得鎖!
線程1,獲得鎖!
線程1,獲得鎖!
線程1,獲得鎖!
線程1,獲得鎖!
線程1,獲得鎖!
線程1,獲得鎖! 線程2,獲得鎖! 線程2,獲得鎖! 線程2,獲得鎖! 線程2,獲得鎖!
復制代碼

 

  修改重入鎖是否公平,觀察輸出結果,如果公平,輸出結果始終為兩個線程交替的獲得鎖,如果是非公平,輸出結果為一個線程占用鎖很長時間,然后才會釋放鎖,另個線程才能執行。

  引出第二個問題:為什么公平鎖例子中出現,公平鎖線程是不斷切換的,而非公平鎖出現同一線程連續獲取鎖的情況?

   何為重進入(重入)?

  重進入是指任意線程在獲取到鎖之后能夠再次獲取該鎖而不會被鎖阻塞,該特性的實現需要解決以下兩個問題:

  • 線程再次獲取鎖:鎖需要去識別獲取鎖的線程是否為當前占據鎖的線程,如果是,則再次成功獲取。
  • 鎖的最終釋放。線程重復 n 次獲取了鎖,隨后在第 n 次釋放該鎖后,其它線程能夠獲取到該鎖。鎖的最終釋放要求鎖對於獲取進行計數自增,計數表示當前鎖被重復獲取的次數,而鎖被釋放時,計數自減,當計數等於 0 時表示鎖已經成功釋放。

以非公平鎖源碼分析:

復制代碼
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
復制代碼

  acquireQueued 方法增加了再次獲取同步狀態的處理邏輯:通過判斷當前線程是否為獲取鎖的線程,來決定獲取操作是否成功,如果獲取鎖的線程再次請求,則將同步狀態值進行增加並返回 true,表示獲取同步狀態成功。
成功獲取鎖的線程再次獲取鎖,只是增加了同步狀態值,也就是要求 ReentrantLock 在釋放同步狀態時減少同步狀態值,釋放鎖源碼如下:

復制代碼
public void unlock() { sync.release(1); } public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } 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); } setState(c); return free; }
復制代碼

  如果鎖被獲取 n 次,那么前 (n-1) 次 tryRelease(int releases) 方法必須返回 false,只有同步狀態完全釋放了,才能返回 true。該方法將同步狀態是否為 0 作為最終釋放的條件,當同步狀態為 0 時,將占有線程設置為 null,並返回 true,表示釋放成功。

通過對獲取與釋放的分析,就可以解釋,以上兩個例子中出現的兩個問題:為什么 ReentrantLock 鎖能夠支持一個線程對資源的重復加鎖?為什么公平鎖例子中出現,公平鎖線程是不斷切換的,而非公平鎖出現同一線程連續獲取鎖的情況?

  • 為什么支持重復加鎖?因為源碼中用變量 c 來保存當前鎖被獲取了多少次,故在釋放時,對 c 變量進行減操作,只有 c 變量為 0 時,才算鎖的最終釋放。所以可以 lock 多次,同時 unlock 也必須與 lock 同樣的次數。
  • 為什么非公平鎖出現同一線程連續獲取鎖的情況?tryAcquire 方法中增加了再次獲取同步狀態的處理邏輯;

    小結

    對上面ReentrantLock的幾個重要方法整理如下:

    • lock():獲得鎖,如果鎖被占用,進入等待。
    • lockInterruptibly():獲得鎖,但優先響應中斷。
    • tryLock():嘗試獲得鎖,如果成功,立即放回 true,反之失敗返回 false。該方法不會進行等待,立即返回。
    • tryLock(long time, TimeUnit unit):在給定的時間內嘗試獲得鎖。
    • unLock():釋放鎖。

    對於其實現原理,下篇博文將詳細分析,其主要包含三個要素:

    • 原子狀態:原子狀態有 CAS(compareAndSetState) 操作來存儲當前鎖的狀態,判斷鎖是否有其他線程持有。
    • 等待隊列:所有沒有請求到鎖的線程,會進入等待隊列進行等待。待有線程釋放鎖后,系統才能夠從等待隊列中喚醒一個線程,繼續工作。詳見:隊列同步器——AQS(待更新)
    • 阻塞原語 park() 和 unpark(),用來掛起和恢復線程。沒有得到鎖的線程將會被掛起。關於阻塞原語,詳見:線程阻塞工具類——LockSupport(待更新)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM