在看這篇文章時,筆者默認你已經看過AQS或者已經初步的瞭解AQS的內部過程。html
先簡單介紹一下ReentantLock
,跟synchronized
相同,是可重入的重量級鎖。可是其用法則至關不一樣,首先ReentrantLock
要顯式的調用lock方法表示接下來的這段代碼已經被當前線程鎖住,其餘線程須要執行時須要拿到這個鎖才能執行,而當前線程在執行完以後要顯式的釋放鎖,固定格式java
lock.lock(); try { doSomething(); } finally { lock.unlock(); }
來經過下面這段代碼簡單的瞭解ReentrantLock
是如何使用的框架
// 定義一個鎖 private static Lock lock = new ReentrantLock(); /** * ReentrantLock的使用例子,而且驗證其一些特性 * @param args 入參 * @throws Exception 錯誤 */ public static void main(String[] args) throws Exception { // 線程池 ThreadPoolExecutor executor = ThreadPoolUtil.getInstance(); executor.execute(() -> { System.err.println("線程1嘗試獲取lock鎖..."); lock.lock(); try { System.err.println("線程1拿到鎖並進入try,準備執行testForLock方法"); // 調用下方的方法,驗證lock的可重入性 testForLock(); TimeUnit.MILLISECONDS.sleep(500); System.err.println("線程1try模塊所有執行完畢,準備釋放lock鎖"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); System.err.println("線程1釋放lock鎖,線程1釋放鎖2次,此時纔算真正釋放,驗證了ReentrantLock加鎖多少次就要釋放多少次鎖"); } }); // 先睡他100ms,保證線程1先拿到鎖 TimeUnit.MILLISECONDS.sleep(100); executor.execute(() -> { System.err.println("線程2嘗試獲取lock鎖..."); lock.lock(); try { System.err.println("線程2拿到鎖並進入try"); } finally { lock.unlock(); System.err.println("線程2執行完畢,釋放lock鎖"); } }); } /** * 驗證ReentrantLock具備可重入 */ public static void testForLock() throws InterruptedException { System.err.println("線程1開始執行testForLock方法,正準備獲取lock鎖..."); lock.lock(); try { System.err.println("testForLock成功獲取lock鎖,證實了ReentrantLock具備可重入性"); TimeUnit.MILLISECONDS.sleep(200); } finally { lock.unlock(); System.err.println("testForLock釋放lock鎖,線程1釋放鎖一次"); } }
結果圖:ui
從結果圖中,咱們獲得了不少信息,好比ReentrantLock
具有可重入性(testForLock
方法得出),而且其釋放鎖的次數必須跟加鎖的次數保持一致(這樣才能保證正確性);此外ReentrantLock
爲悲觀鎖,在某個線程獲取到鎖以後其餘線程在其徹底釋放以前不得獲取(線程2充分證實了這一點,其開始獲取鎖的時間要比線程1的執行時間快許多,但仍是被阻塞住了)。線程
okay,那來看下其內部是如何實現的,直接點擊lock()
方法code
public void lock() { sync.lock(); }
看到其直接調用了sync
的lock()
方法,再點擊進入htm
abstract static class Sync extends AbstractQueuedSynchronizer { // ... abstract void lock(); // ... }
能夠看到Sync
類是ReentrantLock
的一個內部類,繼承了AQS
框架,也就是說ReentrantLock
就是AQS框架下的一個產物,那麼問題就變得簡單起來了。若是還沒了解過AQS
的能夠看下我另外一篇文章——AQS框架詳解,看過以後再回頭看ReentrantLock
,你會發現,就這?blog
扯回來ReentrantLock
,這邊能夠看到內部類Sync
是一個抽象類,lock()
方法也是一個抽象方法,也就意味着這個lock
會根據子類的不一樣實現執行不一樣操做,點開子類發現有兩個——公平鎖和非公平鎖。繼承
裏邊的具體實現先放一放,回到ReentrantLock
的lock
方法隊列
public void lock() { sync.lock(); }
直接調用說明sync
已經被初始化過,那麼在哪裏進行初始化的呢?仔細翻一翻能夠從ReentrantLock
的兩個構造方法中發現貓膩
/** * 構造方法1 * 無參構造方法,直接將sync初始化爲非公平鎖 */ public ReentrantLock() { sync = new NonfairSync(); } /** * 構造方法2 * 帶參構造方法,根據傳進來的布爾值決定將sync初始化爲公平仍是非公平鎖 */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
這裏順帶說一下,在AQS
有一個同步隊列(CLH
),是一種先進先出隊列。公平鎖的意思就是嚴格按照這個隊列的順序來獲取鎖,非公平鎖的意思就是不必定按照這個隊列的順序來。
那如今知道sync
是在建立ReentrantLock
的時候就進行了初始化,咱們就來看下公平和非公平鎖各自作了什麼吧。
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; final void lock() { // 使用CAS嘗試將state改成1,若是成功了,則表示獲取鎖成功,設置當前線程爲持有線程便可 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else // 不然的話調用AQS的acquire方法乖乖入同步隊列等待去吧 acquire(1); } // AQS暴露出來須要子類重寫的方法 protected final boolean tryAcquire(int acquires) { // 方法解釋在下方 return nonfairTryAcquire(acquires); } } // 非公平鎖的tryAcquire方法,該方法是放在Sync抽象類中的,爲了tryLock的時候使用 final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); // 當前鎖的狀態 int c = getState(); // 若是是0則表示鎖是開放狀態,能夠爭奪 if (c == 0) { // 使用CAS設置爲對應的值,在ReentrantLock中acquires的值一直是1 if (compareAndSetState(0, acquires)) { // 成功了設置持有線程 setExclusiveOwnerThread(current); return true; } } /* * 若是當前線程是持有線程,那麼state的值+1 * 這裏也是ReentrantLock可重入的原理 */ 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; }
非公平鎖基本的流程解釋在上方的代碼中已經在註釋寫出,相信不難看懂。不過有個須要注意的點要說一下,首先要看清楚非公平鎖的定義,它是不必定按照隊列順序來獲取,不是不按照隊列順序獲取。
從上面的代碼咱們也能夠看出來,非公平鎖調用lock()
方法的時候會先調用一次CAS
來獲取鎖,成功了直接返回,這第一次操做沒有按照隊列的順序來,但也只有這一次。若是失敗了,入隊以後仍是乖乖的得按照CLH同步隊列的順序來拿鎖,這一點要搞清楚。
static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; // lock方法直接調用AQS的acquire方法,連一點爭取的慾望都沒有 final void lock() { acquire(1); } // 公平鎖的獲取資源方法,該方法是在acquire方法類調用的 protected final boolean tryAcquire(int acquires) { // 總體邏輯仍是挺簡單的,跟非公平有些相似 final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { /* * c==0表示當前鎖沒有被獲取 * 若是沒有前驅節點或者前驅節點是頭結點, * 那麼使用CAS嘗試獲取資源 * 成功了設置持有線程並返回true,失敗了直接返回 */ if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // 若是當前線程持有鎖,跟非公平鎖一致,可重入 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
公平鎖的邏輯相對來講十分簡單,lock
方法老老實實的去排隊獲取鎖,而獲取資源方法的邏輯也在代碼註釋寫得很清楚了,沒有什麼須要多講的。
上面的理解以後釋放鎖的邏輯就簡單的多了,直接放代碼吧:
/* * 解鎖方法直接調用AQS的release方法 * 而release方法的去向又是跟tryRelease的返回值直接相關 * tryRelease方法的實如今內部類Sync中,具體在下方 */ public void unlock() { sync.release(1); } abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = -5179523762034025860L; // ... // 釋放資源的方法 protected final boolean tryRelease(int releases) { // 拿到當前鎖的加鎖次數 int c = getState() - releases; // 當前線程必須是鎖持有線程才能操做 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // 若是次數爲0,表示徹底釋放,清空持有線程 if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; } // ... }
釋放鎖的邏輯在註釋中解釋得很清楚了,看完也知道因爲ReentrantLock
是可重入的,因此鎖的數值會逐漸增長,那麼在釋放的時候也要一個一個逐一釋放。
主要的邏輯仍是AQS
的release
方法中,這裏詳講的話篇幅太多,有興趣的話能夠單獨看下AQS
的文章,傳送門:AQS。
來說下ReentrantLock
跟Synchonized
的一大不一樣點之一——Condition
。那麼condition
是什麼呢,簡單來講就是將等待獲取資源的線程獨立出來分隊,什麼意思呢?舉個例子,如今有8個線程同時爭取一個鎖,我以爲太多了,就把這個8個線程平均分紅4隊,等我以爲哪隊OK就將那一隊的線程叫出來爭取這個鎖。在這裏的condition
就是隊伍,4隊就是4個condition
。
另外說一句,condition
(隊伍)中的線程是不參與鎖的競爭的,若是上方的8個線程我只將2個線程放入一個隊,其餘線程不創建隊伍,那麼其餘線程會參與鎖的競爭,而獨立到隊伍中的2個線程則不會,由於其被放在AQS
的等待隊列中,等待隊列是不參與資源的競爭的,我在另外一篇文章——AQS框架詳解寫得很清楚了。仍是那句話,AQS
懂了再看ReentrantLock
,理解難度就會低得多得多得多得多....
okay,那來簡單看下Condition
如何使用
// 線程池 ThreadPoolExecutor executor = ThreadPoolUtil.getInstance(); // 這裏只建了一個condition起理解做用,本身有興趣的話能夠多建幾個模擬多點場景 Condition condition = lock.newCondition(); executor.execute(() -> { System.err.println("線程1嘗試獲取lock鎖..."); lock.lock(); try { System.err.println("線程1拿到鎖並進入try"); System.err.println("線程1準備進行condition操做"); /* * 將當前線程即線程1放入指定的這個condition中, * 若是是其餘condition則調用其餘condition的await()方法 */ condition.await(); System.err.println("線程1結束condition操做"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); System.err.println("線程1執行完畢,釋放lock鎖"); } }); // 保證線程1獲取鎖而且執行完畢 TimeUnit.MILLISECONDS.sleep(200); executor.execute(() -> { System.err.println("線程2嘗試獲取lock鎖..."); lock.lock(); try { System.err.println("線程2拿到鎖並進入try"); // 喚醒condition的全部線程 condition.signalAll(); System.err.println("線程2將condition中的線程喚醒"); } finally { lock.unlock(); System.err.println("線程2執行完畢,釋放lock鎖"); } });
結果圖:
能夠從結果圖中看到,
當線程調用了condition.await()
的時候就被放入了condition
中,而且此時將持有的鎖釋放,將本身掛起睡覺等待其餘線程喚醒。因此線程2才能在線程1沒執行完的狀況獲取到了鎖,而且線程2執行完操做以後將線程1喚醒,線程1此時實際上是從新進入同步隊列(隊尾)爭取資源的,若是隊列前方還有線程在等待的話它是不會拿到的,要按照隊列順序獲取,能夠本身在本地創多幾個線程試一下。
經過這段簡單的代碼以後明顯能夠看到condition
具備不錯的靈活性,也就是說提供了更多了選擇性,這也就是跟synchronized
不一樣的地方,若是使用synchronized
加鎖,那麼Object
的喚醒方法只能喚醒所有,或者其中的一個,可是ReentrantLock
不一樣,有了condition
的幫助,能夠不一樣的線程進行不一樣的分組,而後有選擇的喚醒其中的一組或者其中一組的隨機一個。
ReentrantLock
的源碼若是有了AQS
的基礎,那麼看起來是不費吹灰之力(開個玩笑,仍是要比吹灰費勁的)。因此本章的篇幅也比較簡單,先從一個例子說明了ReentrantLock
的用法, 而且經過這個例子介紹了ReentrantLock
可重入、悲觀鎖的幾個特性;接着對其lock
方法進行源碼跟蹤,從而瞭解到其內部的方法都是由繼承AQS
的內部類Sync
來實現的,而Sync
又分紅了兩個類,表明兩種不一樣的鎖——公平鎖和非公平鎖;接下來再講到兩種鎖的具體實現和釋放的邏輯,到這裏加鎖解鎖的流程就完整了;最後再介紹ReentrantLock
的另外一種特性——Condition
,這種特性容許其選擇特定的線程來爭奪鎖,也能夠選擇性的喚醒鎖,到這裏整篇文章就告一段落。
孤獨的人不必定是天才,還多是得了鬱抑症。