被問到ReentrantLock,你真的能答好嗎?
一、先了解一下
我們知道實現一把鎖要有如下幾個邏輯:
- 鎖的標識
- 線程搶鎖的邏輯
- 線程掛起的邏輯
- 線程存儲邏輯
- 線程釋放鎖的邏輯
- 線程喚醒的邏輯
我們在講解AQS的時候說過AQS基本負責了實現鎖的全部邏輯,唯獨線程搶鎖和線程釋放鎖的邏輯是交給子類來實現了,而ReentrantLock作為最常用的獨占鎖,其內部就是包含了AQS的子類實現了線程搶鎖和釋放鎖的邏輯。
我們在使用ReentrantLock的時候一般只會使用如下方法:
ReentrantLock lock=new ReentrantLock();
lock.lock();
lock.unlock();
lock.tryLock();
Condition condition=lock.newCondition();
condition.await();
condition.signal();
condition.signalAll();
二、技術架構
如果我們自己來實現一個鎖,那么如何設計呢?
根據AQS的邏輯,我們寫一個子類sync,這個類一定會調用父類的acquire方法進行上鎖,同時重寫tryAcquire方法實現自己搶鎖邏輯,也一定會調用release方法進行解鎖,同時重寫tryRelease方法實現釋放鎖邏輯。
那么ReentrantLock是怎么實現的呢?
ReentrantLock的實現的類架構如下,ReentrantLock對外提供作為一把鎖應該具備的api,比如lock加鎖,unlock解鎖等等,而它內部真正的實現是通過靜態內部類sync實現,sync是AQS的子類,是真正的鎖,因為這把鎖需要支持公平和非公平的特性,所以sync又有兩個子類FairSync和NonfairSync分別實現公平鎖和非公平鎖。
因為是否公平說的是搶鎖的時候是否公平,那兩個子類就要在上鎖方法acquire的調用和搶鎖方法tryAcquire的重寫上做文章。
公平鎖做了什么文章?
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
}
公平鎖比較簡單,直接調用了父級類AQS的acquire方法,因為AQS的鎖默認就是公平的排隊策略。
重寫tryAcquire方法的邏輯為:
- 判斷當前鎖是否被占用,即state是否為0
- 如果當前鎖沒有被占用,然后會判斷等待隊列中是否有線程在阻塞等待,如果有,那就終止搶鎖,如果沒有,就通過cas搶鎖,搶到鎖返回true,沒有搶到鎖返回false。
- 如果當前鎖已經被占用,然后判斷占用鎖的線程是不是自己,如果是,就會將state加1,表示重入,返回true。如果不是自己那就是代表沒有搶到鎖,返回false。
公平就公平在老老實實排隊。
非公平鎖做了什么文章?
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
//nonfairTryAcquire代碼在父類sync里面
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;
}
非公平鎖也很簡單,沒有直接調用了父級類AQS的acquire方法,而是先通過cas搶鎖,它不管等待隊列中有沒有其他線程在排隊,直接搶鎖,這就體現了不公平。
它重寫tryAcquire方法的邏輯為:
- 判斷當前鎖是否被占用,即state是否為0
- 如果當前鎖沒有被占用,就直接通過cas搶鎖(不管等待隊列中有沒有線程在排隊),搶到鎖返回true,沒有搶到鎖返回false。
- 如果當前鎖已經被占用,然后判斷占用鎖的線程是不是自己,如果是,就會將state加1,表示重入,返回true。如果不是自己那就是代表沒有搶到鎖,返回false。
公平鎖和非公平分別重寫了tryAcquire方法,來滿足公平和非公平的特性。那么tryAcquire方法也是需要子類重寫的,因為它和是否公平無關,因此tryAcquire方法被抽象到sync類中重寫。
sync類中
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;
}
釋放鎖的邏輯如下:
- 獲取state的值,然后減1
- 如果state為0,代表鎖已經釋放,清空aqs中的持有鎖的線程字段的值
- 如果state不為0,說明當前線程重入了,還需要再次釋放鎖
- 將state寫回
釋放鎖往往和搶鎖邏輯是對應的,每個子類搶鎖邏輯不同的話,釋放鎖的邏輯也會對應不同。
三、具體實現
接下來我們通過ReentrantLock的使用看下它的源碼實現:
class X {
private final ReentrantLock lock = new ReentrantLock();
Condition condition1=lock.newCondition();
Condition condition2=lock.newCondition();
public void m() {
lock.lock();
try {
if(條件1){
condition1.await();
}
if(條件2){
condition2.await();
}
} catch (InterruptedException e) {
} finally {
condition1.signal();
condition2.signal();
lock.unlock();
}
}
}
1.先看這個方法:lock.lock()
ReentrantLock類
public void lock() {
sync.lock();
}
NonfairSync 類中
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
FairSync 類中
final void lock() {
acquire(1);
}
公平鎖和非公平鎖中都實現了lock方法,公平鎖直接調用AQS的acquire,而非公平鎖先搶鎖,搶不到鎖再調用AQS的acquire方法進行上鎖
進入acquire方法后的邏輯我們就都知道了。
2.再看這個方法lock.unlock()
public void unlock() {
sync.release(1);
}
unlock方法內直接調用了AQS的Release方法進行解鎖的邏輯,進入release方法后邏輯我們都已經知道了,這里不再往下跟。
3.最后看這個方法lock.tryLock()
這個方法中直接調用了sync中的nonfairTryAcquire方法,這個就是非公平鎖實現的TryAcquire方法,只是名字改了而已。
tryLock方法的意思是嘗試搶鎖,是給程序員調用的嘗試搶鎖方法,如果搶鎖成功返回true,搶鎖失敗返回false,當拿到搶鎖失敗的信號后,程序員可以做一些自己的策略。
如果沒有tryLock方法,而是直接調用lock方法,就會走到acquire方法中,Acquire方法中也會調用TryAcquire方法,只不過在這里如果獲取不到鎖就會入隊阻塞了,就沒有程序員參與的可能了。
4.lock.newCondition();
在AQS那篇我們說過Condition是AQS中的條件隊列,可以按條件將一批線程由不可喚醒變為可喚醒。
ReentrantLock類
public Condition newCondition() {
return sync.newCondition();
}
sync靜態內部類
final ConditionObject newCondition() {
return new ConditionObject();
}
sync提供了創建Condition對象的方法,意味著ReentrantLock也擁有Condition的能力。
四、ReentrantLock和synchronized對比
我們下面說的ReentrantLock其實就是說AQS,因為它的同步實現主要在AQS里面。
1.實現方面
- ReentrantLock是jdk級別實現的,其源碼在jdk源碼中可以查看,沒有脫離java。
- synchronized是jvm級別實現的,synchronized只是java端的一個關鍵字,具體邏輯實現都在jvm中。
2.性能方面
優化前的synchronized性能很差,主要表現在兩個方面:
- 因為大多數情況下對于資源的爭奪并沒有那么激烈,甚至于某個時刻可能只有一個線程在工作,在這種沒有競爭或者競爭壓力很小的情況下,如果每個線程都要進行用戶態到內核態的切換其實是很耗時的。
- jdk1.6對synchronized底層實現做了優化,優化后,在單線程以及并發不是很高的情況下通過無鎖偏向和自旋鎖的方式避免用戶態到內核態的切換,因此性能提高了,優化后的synchronized和ReentrantLock性能差不多了。
ReentrantLock是在jdk實現的,它申請互斥量就是對鎖標識state的爭奪,它是通過cas方式實現。在java端實現。
對于爭奪不到資源的線程依然要阻塞掛起,但凡阻塞掛起都要依賴于操作系統底層,這一步的用戶態到內核態的切換是避免不了的。
因此在單線程進入代碼塊的時候,效率是很高的,因此我們說ReentrantLock性能高于原始的synchronized:
- 申請互斥量:synchronized的鎖其實就是爭奪Monitor鎖的擁有權,這個爭奪過程是通過操作系統底層的互斥原語Mutex實現的,這個過程會有用戶態到內核態的切換。
- 線程阻塞掛起:沒能搶到到Monitor鎖擁有權的線程要阻塞掛起,阻塞掛起這個動作也是依靠操作系統實現的,這個過程也需要用戶態到內核態的切換。
3.特性方面
兩個都是常用的典型的獨占鎖。
- ReentrantLock可重入,可中斷,支持公平和非公平鎖,可嘗試獲取鎖,可以支持分組將線程由不可喚醒變為可喚醒。
- synchronized可重入,不可中斷,非公平鎖,不可嘗試獲取鎖,只支持一個或者全部線程由不可喚醒到可喚醒。
4.使用方面
synchronized不需要手動釋放鎖,ReentrantLock需要手動釋放鎖,需要考慮異常對釋放鎖的影響避免異常導致線程一直持有鎖。
以下是兩個鎖的使用方式:
class X {
private final ReentrantLock lock = new ReentrantLock();
Condition condition1=lock.newCondition();
Condition condition2=lock.newCondition();
public void m() {
lock.lock();
try {
if(1==2){
condition1.await();
}
if(1==3){
condition2.await();
}
} catch (InterruptedException e) {
} finally {
condition1.signal();
condition2.signal();
lock.unlock();
}
}
}
class X {
private final testtest sync=new testtest();;
public void m() throws InterruptedException {
synchronized(sync){
if(1==2){
sync.wait();
}
sync.notify();
sync.notifyAll();
}
}
}
對比代碼及特性說明:
- 兩個鎖都是依賴一個對象:lock和sync
- condition和wait方法具有同樣的效果,進入condition和wait的線程將陷入等待(不可喚醒狀態),只有被分別調用signal和notify方法線程才會重新變為可喚醒狀態,請注意是可喚醒,而不是被喚醒。
- 可喚醒是說具備了競爭資源的資格,資源空閑后,synchronized中會在可喚醒狀態的線程中隨機挑選一個線程去拿鎖,而ReentrantLock中不可喚醒的線程變為可喚醒狀態,其實就是將條件隊列中的線程搬到等待隊列中排隊,只有隊頭的才會去嘗試拿鎖。
- ReentrantLock分批將線程由不可喚醒變為可喚醒也在這段代碼中體現了,代碼中按照不同的條件將線程放入不同的condition,每個condition就是一個組,釋放的時候也可以按照不同的條件進行釋放。而synchronized中進入wait的線程不能分組,釋放也只能隨機釋放一個或者全部釋放。