ReentrantLock原始碼解析——雖眾但寫
阿新 • • 發佈:2020-04-02
> 在看這篇文章時,筆者預設你已經看過AQS或者已經初步的瞭解AQS的內部過程。
先簡單介紹一下`ReentantLock`,跟`synchronized`相同,是**可重入**的重量級鎖。但是其用法則相當不同,首先`ReentrantLock`要**顯式的呼叫lock方法**表示接下來的這段程式碼已經被當前執行緒鎖住,其他執行緒需要執行時需要拿到這個鎖才能執行,而當前執行緒在執行完之後要顯式的釋放鎖,固定格式
``` java
lock.lock();
try {
doSomething();
} finally {
lock.unlock();
}
```
# 1.ReentrantLock的demo程式
來通過下面這段程式碼簡單的瞭解`ReentrantLock`是如何使用的
```java
// 定義一個鎖
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釋放鎖一次");
}
}
```
結果圖:![1585664568146](https://images.cnblogs.com/cnblogs_com/zhangweicheng/1583123/o_2004011435041585664568146.png)
從結果圖中,我們得到了很多資訊,比如`ReentrantLock`具備**可重入性**(`testForLock`方法得出),並且**其釋放鎖的次數必須跟加鎖的次數保持一致**(這樣才能保證正確性);此外`ReentrantLock`為**悲觀鎖**,在某個執行緒獲取到鎖之後其他執行緒在其完全釋放之前不得獲取(執行緒**2**充分證明了這一點,其開始獲取鎖的時間要比執行緒**1**的執行時間快許多,但還是被阻塞住了)。
# 2.獲取鎖的方法——lock()
okay,那來看下其內部是如何實現的,直接點選`lock()`方法
```java
public void lock() {
sync.lock();
}
```
看到其直接呼叫了`sync`的`lock()`方法,再點選進入
```java
abstract static class Sync extends AbstractQueuedSynchronizer {
// ...
abstract void lock();
// ...
}
```
可以看到`Sync`類是`ReentrantLock`的一個**內部類**,繼承了**`AQS`框架**,也就是說`ReentrantLock`就是**AQS框架下的一個產物**,那麼問題就變得簡單起來了。如果還沒了解過`AQS`的可以看下我另一篇文章——[AQS框架詳解](https://www.cnblogs.com/zhangweicheng/p/12000213.html),看過之後再回頭看`ReentrantLock`,你會發現,**就這?**
扯回來`ReentrantLock`,這邊可以看到**內部類**`Sync`是一個抽象類,`lock()`方法也是一個**抽象方法**,也就意味著這個`lock`會根據子類的不同實現執行不同操作,點開子類發現有兩個——**公平鎖和非公平鎖**。
![1585667693048](https://images.cnblogs.com/cnblogs_com/zhangweicheng/1583123/o_2004011440151585667693048.png)
裡邊的具體實現先放一放,回到`ReentrantLock`的`lock`方法
```java
public void lock() {
sync.lock();
}
```
直接呼叫說明`sync`已經被初始化過,那麼在哪裡進行初始化的呢?仔細翻一翻可以從`ReentrantLock`的**兩個構造方法**中發現貓膩
```java
/**
* 構造方法1
* 無參構造方法,直接將sync初始化為非公平鎖
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* 構造方法2
* 帶參構造方法,根據傳進來的布林值決定將sync初始化為公平還是非公平鎖
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
```
這裡順帶說一下,在`AQS`有一個**同步佇列(`CLH`)**,是一種**先進先出佇列**。公平鎖的意思就是**嚴格按照這個佇列的順序來獲取鎖,非公平鎖的意思就是不一定按照這個佇列的順序來。**
那現在知道`sync`是在建立`ReentrantLock`的時候就進行了初始化,我們就來看下公平和非公平鎖各自做了什麼吧。
## 2.1 非公平鎖
```java
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同步佇列的順序來拿鎖,**這一點要搞清楚。
## 2.3 公平鎖
```java
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`方法老老實實的去排隊獲取鎖,而獲取資源方法的邏輯也在程式碼註釋寫得很清楚了,沒有什麼需要多講的。
# 3.鎖釋放
上面的理解之後釋放鎖的邏輯就簡單的多了,直接放程式碼吧:
```java
/*
* 解鎖方法直接呼叫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](https://www.cnblogs.com/zhangweicheng/p/12000213.html)。
# 4.ReentrantLock的可選擇性
來講下`ReentrantLock`跟`Synchonized`的一大不同點之一——`Condition`。那麼`condition`是什麼呢,簡單來說就是將**等待獲取資源的執行緒獨立出來分隊**,什麼意思呢?舉個例子,現在有8個執行緒同時爭取一個鎖,我覺得太多了,就把這個8個執行緒平均分成4隊,等我覺得哪隊OK就將那一隊的執行緒叫出來爭取這個鎖。在這裡的`condition`就是隊伍,4隊就是4個`condition`。
另外說一句,**`condition`(隊伍)中的執行緒是不參與鎖的競爭**的,如果上方的8個執行緒我只將2個執行緒放入一個隊,其他執行緒不建立隊伍,那麼**其他執行緒會參與鎖的競爭,而獨立到隊伍中的2個執行緒則不會**,因為其被放在`AQS`的**等待佇列**中,**等待佇列是不參與資源的競爭**的,我在另一篇文章——[AQS框架詳解](https://www.cnblogs.com/zhangweicheng/p/12000213.html)寫得很清楚了。還是那句話,`AQS`懂了再看`ReentrantLock`,理解難度就會低得多得多得多得多....
okay,那來簡單看下`Condition`如何使用
```java
// 執行緒池
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鎖");
}
});
```
結果圖:
![1585749193417](https://images.cnblogs.com/cnblogs_com/zhangweicheng/1583123/o_2004011451411585749193417.png)
可以從結果圖中看到,
當執行緒呼叫了`condition.await()`的時候就被放入了`condition`中,並且此時**將持有的鎖釋放,將自己掛起睡覺等待其他執行緒喚醒。**所以執行緒2才能線上程1沒執行完的情況獲取到了鎖,並且執行緒2執行完操作之後將執行緒1喚醒,執行緒1此時其實是**重新進入同步佇列(隊尾)爭取資源**的,如果佇列前方還有執行緒在等待的話它是不會拿到的,要按照佇列順序獲取,可以自己在本地創多幾個執行緒試一下。
通過這段簡單的程式碼之後明顯可以看到`condition`具有不錯的靈活性,也就是說提供了更多了**選擇性**,這也就是跟`synchronized`不同的地方,如果使用`synchronized`加鎖,那麼`Object`的喚醒方法只能喚醒全部,或者其中的一個,但是`ReentrantLock`不同,有了`condition`的幫助,可以不同的執行緒進行不同的分組,然後有選擇的**喚醒其中的一組**或者**其中一組的隨機一個。**
# 5.總結
`ReentrantLock`的原始碼如果有了`AQS`的基礎,那麼看起來是不費吹灰之力(開個玩笑,還是要比吹灰費勁的)。所以本章的篇幅也比較簡單,先從一個例子說明了`ReentrantLock`的用法, 並且通過這個例子介紹了`ReentrantLock`**可重入、悲觀鎖**的幾個特性;接著對其`lock`方法進行原始碼跟蹤,從而瞭解到其內部的方法都是由繼承`AQS`的內部類`Sync`來實現的,而`Sync`又分成了兩個類,**代表兩種不同的鎖**——**公平鎖和非公平鎖**;接下來再講到兩種鎖的具體實現和釋放的邏輯,到這裡加鎖解鎖的流程就完整了;最後再介紹`ReentrantLock`的另一種特性——`Condition`,這種特性允許其選擇特定的執行緒來爭奪鎖,也可以選擇性的喚醒鎖,到這裡整篇文章就告一段落。
> 孤獨的人不一定是天才,還可能是得了鬱