面試中如何答好:ReentrantLock
先了解一下
讀本篇前,一定要確保已經(jīng)讀過本公眾號(hào)的AQS講解。
我們知道實(shí)現(xiàn)一把鎖要有如下幾個(gè)邏輯
- 鎖的標(biāo)識(shí)
- 線程搶鎖的邏輯
- 線程掛起的邏輯
- 線程存儲(chǔ)邏輯
- 線程釋放鎖的邏輯
- 線程喚醒的邏輯
我們?cè)谥v解AQS的時(shí)候說過AQS基本負(fù)責(zé)了實(shí)現(xiàn)鎖的全部邏輯,唯獨(dú)線程搶鎖和線程釋放鎖的邏輯是交給子類來實(shí)現(xiàn)了,而ReentrantLock作為最常用的獨(dú)占鎖,其內(nèi)部就是包含了AQS的子類實(shí)現(xiàn)了線程搶鎖和釋放鎖的邏輯。
我們?cè)谑褂肦eentrantLock的時(shí)候一般只會(huì)使用如下方法:
ReentrantLock lock=new ReentrantLock();
lock.lock();
lock.unlock();
lock.tryLock();
Condition condition=lock.newCondition();
condition.await();
condition.signal();
condition.signalAll();
技術(shù)架構(gòu)
如果我們自己來實(shí)現(xiàn)一個(gè)鎖,那么如何設(shè)計(jì)呢?
根據(jù)AQS的邏輯,我們寫一個(gè)子類sync,這個(gè)類一定會(huì)調(diào)用父類的acquire方法進(jìn)行上鎖,同時(shí)重寫tryAcquire方法實(shí)現(xiàn)自己搶鎖邏輯,也一定會(huì)調(diào)用release方法進(jìn)行解鎖,同時(shí)重寫tryRelease方法實(shí)現(xiàn)釋放鎖邏輯。
圖片
那么ReentrantLock是怎么實(shí)現(xiàn)的呢?
ReentrantLock的實(shí)現(xiàn)的類架構(gòu)如下,ReentrantLock對(duì)外提供作為一把鎖應(yīng)該具備的api,比如lock加鎖,unlock解鎖等等,而它內(nèi)部真正的實(shí)現(xiàn)是通過靜態(tài)內(nèi)部類sync實(shí)現(xiàn),sync是AQS的子類,是真正的鎖,因?yàn)檫@把鎖需要支持公平和非公平的特性,所以sync又有兩個(gè)子類FairSync和NonfairSync分別實(shí)現(xiàn)公平鎖和非公平鎖。
圖片
因?yàn)槭欠窆秸f的是搶鎖的時(shí)候是否公平,那兩個(gè)子類就要在上鎖方法acquire的調(diào)用和搶鎖方法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;
}
}
公平鎖比較簡單,直接調(diào)用了父級(jí)類AQS的acquire方法,因?yàn)锳QS的鎖默認(rèn)就是公平的排隊(duì)策略。
重寫tryAcquire方法的邏輯為:
- 判斷當(dāng)前鎖是否被占用,即state是否為0
- 如果當(dāng)前鎖沒有被占用,然后會(huì)判斷等待隊(duì)列中是否有線程在阻塞等待,如果有,那就終止搶鎖,如果沒有,就通過cas搶鎖,搶到鎖返回true,沒有搶到鎖返回false。
- 如果當(dāng)前鎖已經(jīng)被占用,然后判斷占用鎖的線程是不是自己,如果是,就會(huì)將state加1,表示重入,返回true。如果不是自己那就是代表沒有搶到鎖,返回false。
公平就公平在老老實(shí)實(shí)排隊(duì)。
非公平鎖做了什么文章?
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;
}
非公平鎖也很簡單,沒有直接調(diào)用了父級(jí)類AQS的acquire方法,而是先通過cas搶鎖,它不管等待隊(duì)列中有沒有其他線程在排隊(duì),直接搶鎖,這就體現(xiàn)了不公平。
它重寫tryAcquire方法的邏輯為:
- 判斷當(dāng)前鎖是否被占用,即state是否為0
- 如果當(dāng)前鎖沒有被占用,就直接通過cas搶鎖(不管等待隊(duì)列中有沒有線程在排隊(duì)),搶到鎖返回true,沒有搶到鎖返回false。
- 如果當(dāng)前鎖已經(jīng)被占用,然后判斷占用鎖的線程是不是自己,如果是,就會(huì)將state加1,表示重入,返回true。如果不是自己那就是代表沒有搶到鎖,返回false。
公平鎖和非公平分別重寫了tryAcquire方法,來滿足公平和非公平的特性。那么tryAcquire方法也是需要子類重寫的,因?yàn)樗褪欠窆綗o關(guān),因此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,代表鎖已經(jīng)釋放,清空aqs中的持有鎖的線程字段的值
- 如果state不為0,說明當(dāng)前線程重入了,還需要再次釋放鎖
- 將state寫回
釋放鎖往往和搶鎖邏輯是對(duì)應(yīng)的,每個(gè)子類搶鎖邏輯不同的話,釋放鎖的邏輯也會(huì)對(duì)應(yīng)不同。
具體實(shí)現(xiàn)
接下來我們通過ReentrantLock的使用看下它的源碼實(shí)現(xiàn)
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();
}
}
}
先看這個(gè)方法: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);
}
公平鎖和非公平鎖中都實(shí)現(xiàn)了lock方法,公平鎖直接調(diào)用AQS的acquire,而非公平鎖先搶鎖,搶不到鎖再調(diào)用AQS的acquire方法進(jìn)行上鎖
進(jìn)入acquire方法后的邏輯我們就都知道了。
再看這個(gè)方法lock.unlock()
public void unlock() {
sync.release(1);
}
unlock方法內(nèi)直接調(diào)用了AQS的Release方法進(jìn)行解鎖的邏輯,進(jìn)入release方法后邏輯我們都已經(jīng)知道了,這里不再往下跟。
最后看這個(gè)方法lock.tryLock()
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
tryLock方法直接調(diào)用sync的tryAcquireNanos方法,看過AQS的應(yīng)該知道tryAcquireNanos這個(gè)方法是父類AQS的方法,這個(gè)方法和AQS中的四個(gè)核心方法中的Acquire方法一樣都是上鎖的方法,無非是上鎖的那幾個(gè)步驟,調(diào)用tryAcquire方法嘗試搶鎖,搶不到鎖就會(huì)進(jìn)入doAcquireNanos方法。
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
doAcquireNanos這個(gè)方法做的其實(shí)就是入隊(duì),阻塞等一系列上鎖操作,邏輯和Acquire方法中差不多,但是有兩點(diǎn)不同:
- 該方法支持阻塞指定時(shí)長。
- 該方法支持中斷拋異常。
看下下面的代碼
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
這里的阻塞不再是LockSupport類的park方法,而是parkNanos方法,這個(gè)方法支持指定時(shí)長的阻塞,AQS正是利用這個(gè)方法實(shí)現(xiàn)阻塞指定時(shí)長,當(dāng)自動(dòng)喚醒后,循環(huán)中會(huì)判斷是否超過設(shè)定時(shí)長,如果超過直接返回false跳出循環(huán)。
在阻塞期間,如果線程被中斷,就會(huì)拋出異常,同樣會(huì)跳出循環(huán),外面可以通過捕獲這個(gè)異常達(dá)到中斷阻塞的目的。
可見ReentrantLock其實(shí)啥也沒做,其tryLock方法完全是依賴AQS實(shí)現(xiàn)。
lock.newCondition();
在AQS那篇我們說過Condition是AQS中的條件隊(duì)列,可以按條件將一批線程由不可喚醒變?yōu)榭蓡拘选?/p>
ReentrantLock類
public Condition newCondition() {
return sync.newCondition();
}
sync靜態(tài)內(nèi)部類
final ConditionObject newCondition() {
return new ConditionObject();
}
sync提供了創(chuàng)建Condition對(duì)象的方法,意味著ReentrantLock也擁有Condition的能力。
ReentrantLock和synchronized對(duì)比
我們下面說的ReentrantLock其實(shí)就是說AQS,因?yàn)樗耐綄?shí)現(xiàn)主要在AQS里面。
- 實(shí)現(xiàn)方面
ReentrantLock是jdk級(jí)別實(shí)現(xiàn)的,其源碼在jdk源碼中可以查看,沒有脫離java。
synchronized是jvm級(jí)別實(shí)現(xiàn)的,synchronized只是java端的一個(gè)關(guān)鍵字,具體邏輯實(shí)現(xiàn)都在jvm中。 - 性能方面
優(yōu)化前的synchronized性能很差,主要表現(xiàn)在兩個(gè)方面:
因?yàn)榇蠖鄶?shù)情況下對(duì)于資源的爭奪并沒有那么激烈,甚至于某個(gè)時(shí)刻可能只有一個(gè)線程在工作,在這種沒有競爭或者競爭壓力很小的情況下,如果每個(gè)線程都要進(jìn)行用戶態(tài)到內(nèi)核態(tài)的切換其實(shí)是很耗時(shí)的。
jdk1.6對(duì)synchronized底層實(shí)現(xiàn)做了優(yōu)化,優(yōu)化后,在單線程以及并發(fā)不是很高的情況下通過無鎖偏向和自旋鎖的方式避免用戶態(tài)到內(nèi)核態(tài)的切換,因此性能提高了,優(yōu)化后的synchronized和ReentrantLock性能差不多了。
ReentrantLock是在jdk實(shí)現(xiàn)的,它申請(qǐng)互斥量就是對(duì)鎖標(biāo)識(shí)state的爭奪,它是通過cas方式實(shí)現(xiàn)。在java端實(shí)現(xiàn)。
對(duì)于爭奪不到資源的線程依然要阻塞掛起,但凡阻塞掛起都要依賴于操作系統(tǒng)底層,這一步的用戶態(tài)到內(nèi)核態(tài)的切換是避免不了的。
因此在單線程進(jìn)入代碼塊的時(shí)候,效率是很高的,因此我們說ReentrantLock性能高于原始的synchronized
- 申請(qǐng)互斥量
synchronized的鎖其實(shí)就是爭奪Monitor鎖的擁有權(quán),這個(gè)爭奪過程是通過操作系統(tǒng)底層的互斥原語Mutex實(shí)現(xiàn)的,這個(gè)過程會(huì)有用戶態(tài)到內(nèi)核態(tài)的切換。 - 線程阻塞掛起
沒能搶到到Monitor鎖擁有權(quán)的線程要阻塞掛起,阻塞掛起這個(gè)動(dòng)作也是依靠操作系統(tǒng)實(shí)現(xiàn)的,這個(gè)過程也需要用戶態(tài)到內(nèi)核態(tài)的切換。
- 特性方面
兩個(gè)都是常用的典型的獨(dú)占鎖。
ReentrantLock可重入,可中斷,支持公平和非公平鎖,可嘗試獲取鎖,可以支持分組將線程由不可喚醒變?yōu)榭蓡拘选?br>synchronized可重入,不可中斷,非公平鎖,不可嘗試獲取鎖,只支持一個(gè)或者全部線程由不可喚醒到可喚醒。 - 使用方面
synchronized不需要手動(dòng)釋放鎖,ReentrantLock需要手動(dòng)釋放鎖,需要考慮異常對(duì)釋放鎖的影響避免異常導(dǎo)致線程一直持有鎖。
以下是兩個(gè)鎖的使用方式
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();
}
}
}
對(duì)比代碼及特性說明:
- 兩個(gè)鎖都是依賴一個(gè)對(duì)象:lock和sync
- condition和wait方法具有同樣的效果,進(jìn)入condition和wait的線程將陷入等待(不可喚醒狀態(tài)),只有被分別調(diào)用signal和notify方法線程才會(huì)重新變?yōu)榭蓡拘褷顟B(tài),請(qǐng)注意是可喚醒,而不是被喚醒。
- 可喚醒是說具備了競爭資源的資格,資源空閑后,synchronized中會(huì)在可喚醒狀態(tài)的線程中隨機(jī)挑選一個(gè)線程去拿鎖,而ReentrantLock中不可喚醒的線程變?yōu)榭蓡拘褷顟B(tài),其實(shí)就是將條件隊(duì)列中的線程搬到等待隊(duì)列中排隊(duì),只有隊(duì)頭的才會(huì)去嘗試拿鎖。
- ReentrantLock分批將線程由不可喚醒變?yōu)榭蓡拘岩苍谶@段代碼中體現(xiàn)了,代碼中按照不同的條件將線程放入不同的condition,每個(gè)condition就是一個(gè)組,釋放的時(shí)候也可以按照不同的條件進(jìn)行釋放。而synchronized中進(jìn)入wait的線程不能分組,釋放也只能隨機(jī)釋放一個(gè)或者全部釋放。