在一個村子裏面,有一口井水,水質很是的好,村民們都想打井裏的水。這井只有一口,村裏的人那麼多,因此得出個打水的規則才行。村長絞盡腦汁,最終想出了一個比較合理的方案,我們來仔細的看看聰明的村長大人的智慧。java
井邊安排一個看井人,維護打水的秩序。編程
打水時,以家庭爲單位,哪一個家庭任何人先到井邊,就能夠先打水,並且若是一個家庭佔到了打水權,其家人這時候過來打水不用排隊。而那些沒有搶佔到打水權的人,一個一個挨着在井邊排成一隊,先到的排在前面。打水示意圖以下 :多線程
是否是感受很和諧,若是打水的人打完了,他會跟看井人報告,看井人會讓第二我的接着打水。這樣你們總都可以打到水。是否是看起來挺公平的,先到的人先打水,固然不是絕對公平的,本身看看下面這個場景 :併發
看着,一個有娃的父親正在打水,他的娃也到井邊了,因此女憑父貴直接排到最前面打水,羨煞旁人了。
以上這個故事模型就是所謂的公平鎖模型,當一我的想到井邊打水,而如今打水的人又不是自家人,這時候就得乖乖在隊列後面排隊。函數
事情總不是那麼一路順風的,總會有些人想走捷徑,話說看井人年紀大了,有時候,眼力不是很好,這時候,人們開始打起了新主意。新來打水的人,他們看到有人排隊打水的時候,他們不會那麼乖巧的就排到最後面去排隊,反之,他們會看看如今有沒有人正在打水,若是有人在打水,沒輒了,只好排到隊列最後面,但若是這時候前面打水的人剛剛打完水,正在交接中,排在隊頭的人尚未完成交接工做,這時候,新來的人能夠嘗試搶打水權,若是搶到了,呵呵,其餘人也只能睜一隻眼閉一隻眼,由於你們都默認這個規則了。這就是所謂的非公平鎖模型。新來的人不必定總得乖乖排隊,這也就形成了原來隊列中排隊的人可能要等好久好久。
工具
ReentrantLock支持兩種獲取鎖的方式,一種是公平模型,一種是非公平模型。在繼續以前,我們先把故事元素轉換爲程序元素。源碼分析
我們先來講說公平鎖模型:性能
初始化時, state=0,表示無人搶佔了打水權。這時候,村民A來打水(A線程請求鎖),佔了打水權,把state+1,以下所示:學習
線程A取得了鎖,把 state原子性+1,這時候state被改成1,A線程繼續執行其餘任務,而後來了村民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把此鎖所有釋放了,狀態值減到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/
ReentrantLock 是一個可重入的互斥(/獨佔)鎖,又稱爲「獨佔鎖」。
ReentrantLock經過自定義隊列同步器(AQS-AbstractQueuedSychronized,是實現鎖的關鍵)來實現鎖的獲取與釋放。
其能夠徹底替代 synchronized 關鍵字。JDK 5.0 早期版本,其性能遠好於 synchronized,但 JDK 6.0 開始,JDK 對 synchronized 作了大量的優化,使得二者差距並不大。
「獨佔」,就是在同一時刻只能有一個線程獲取到鎖,而其它獲取鎖的線程只能處於同步隊列中等待,只有獲取鎖的線程釋放了鎖,後繼的線程纔可以獲取鎖。
「可重入」,就是支持重進入的鎖,它表示該鎖可以支持一個線程對資源的重複加鎖。
該鎖還支持獲取鎖時的公平和非公平性選擇。「公平」是指「不一樣的線程獲取鎖的機制是公平的」,而「不公平」是指「不一樣的線程獲取鎖的機制是非公平的」。
對於 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,從而繼續執行下去。
執行代碼,輸出以下:
除了等待外部通知(中斷操做 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,當前線程不會進行等待。這種模式不會引發線程等待,所以也不會產生死鎖。
·默認狀況下,鎖的申請都是非公平的。也就是說,若是線程 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,得到鎖!
修改重入鎖是否公平,觀察輸出結果,若是公平,輸出結果始終爲兩個線程交替的得到鎖,若是是非公平,輸出結果爲一個線程佔用鎖很長時間,而後纔會釋放鎖,另個線程才能執行。
引出第二個問題:爲何公平鎖例子中出現,公平鎖線程是不斷切換的,而非公平鎖出現同一線程連續獲取鎖的狀況?
重進入是指任意線程在獲取到鎖以後可以再次獲取該鎖而不會被鎖阻塞,該特性的實現須要解決如下兩個問題:
以非公平鎖源碼分析:
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 鎖可以支持一個線程對資源的重複加鎖?爲何公平鎖例子中出現,公平鎖線程是不斷切換的,而非公平鎖出現同一線程連續獲取鎖的狀況?
對上面ReentrantLock的幾個重要方法整理以下:
對於其實現原理,下篇博文將詳細分析,其主要包含三個要素: