reentrantlock非公平锁不会随机挂起线程?_【原创】Java并发编程系列16 | 公平锁与非公平锁...
本文為何適原創(chuàng)并發(fā)編程系列第 16 篇,文末有本系列文章匯總。
上一篇提到重入鎖 ReentrantLock 支持兩種鎖,公平鎖與非公平鎖。那么這篇文章就來介紹一下公平鎖與非公平鎖。
- 為什么需要公平鎖?
- ReentrantLock 如何是實現(xiàn)公平鎖和非公平鎖的?
- 公平鎖和非公平鎖又都有什么優(yōu)缺點呢?
1. 為什么需要公平鎖
饑餓
我們知道 CPU 會根據(jù)不同的調(diào)度算法進(jìn)行線程調(diào)度,將時間片分派給線程,那么就可能存在一個問題:某個線程可能一直得不到 CPU 分配的時間片,也就不能執(zhí)行。
一個線程因為得不到 CPU 運行時間,就會處于饑餓狀態(tài)。如果該線程一直得不到 CPU 運行時間的機會,最終會被“饑餓致死”。
1.1 導(dǎo)致線程饑餓的原因
每個線程都有獨自的線程優(yōu)先級,優(yōu)先級越高的線程獲得的 CPU 時間越多,如果并發(fā)狀態(tài)下的線程包括一個低優(yōu)先級的線程和多個高優(yōu)先級的線程,那么這個低優(yōu)先級的線程就有可能因為得不到 CPU 時間而饑餓。
當(dāng)同步鎖被占用,線程處在 BLOCKED 狀態(tài)等鎖。當(dāng)鎖被釋放,處在 BLOCKED 狀態(tài)的線程都會去搶鎖,搶到鎖的線程可以執(zhí)行,未搶到鎖的線程繼續(xù)在 BLOCKED 狀態(tài)阻塞。問題在于這個搶鎖過程中,到底哪個線程能搶到鎖是沒有任何保障的,這就意味著理論上是會有一個線程會一直搶不到鎖,那么它將會永遠(yuǎn)阻塞下去的,導(dǎo)致饑餓。
當(dāng)一個線程調(diào)用 Object.wait()之后會被阻塞,直到被 Object.notify()喚醒。而 Object.notify()是隨機選取一個線程喚醒的,不能保證哪一個線程會獲得喚醒。因此如果多個線程都在一個對象的 wait()上阻塞,在沒有調(diào)用足夠多的 Object.notify()時,理論上是會有一個線程因為一直得不到喚醒而處于 WAITING 狀態(tài)的,從而導(dǎo)致饑餓。
1.2 解決饑餓
解決饑餓的方案被稱之為公平性,即所有線程能公平地獲得運行機會。
公平性針對獲取鎖而言的,如果一個鎖是公平的,那么鎖的獲取順序就應(yīng)該符合請求上的絕對時間順序,滿足 FIFO。
2. 公平鎖和非公平鎖的實現(xiàn)
溫馨提示:在理解了上一篇 AQS 實現(xiàn) ReentrantLock 的原理之后,學(xué)習(xí)公平鎖和非公平鎖的實現(xiàn)會很容易。
ReentrantLock 的類結(jié)構(gòu):
public class ReentrantLock implements Lock, java.io.Serializable {private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {}
static final class FairSync extends Sync {}
static final class NonfairSync extends Sync {}
}
ReentrantLock 鎖是由 sync 來管理的,而 Sync 是抽象類,所以 sync 只能是 NonfairSync(非公平鎖)和 FairSync(公平鎖)中的一種,也就是說重入鎖 ReentrantLock 要么是非公平鎖,要么是公平鎖。
ReentrantLock 在構(gòu)造時,就已經(jīng)選擇好是公平鎖還是非公平鎖了,默認(rèn)是非公平鎖。源碼如下:
public ReentrantLock() {sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
上一篇講解了重入鎖實現(xiàn)同步過程:
獲取鎖的方法調(diào)用棧:lock()--> acquire()--> tryAcquire()--> acquire()
acquire()是父類 AQS 的方法,公平鎖與非公平鎖都一樣,不同之處在于 lock()和 tryAcquire()。
lock()方法源碼:
// 公平鎖FairSyncfinal void lock() {
acquire(1);
}
// 非公平鎖NonfairSync
final void lock() {
// 在調(diào)用acquire()方法獲取鎖之前,先CAS搶鎖
if (compareAndSetState(0, 1)) // state=0時,CAS設(shè)置state=1
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
可以看到,非公平鎖在調(diào)用 acquire()方法獲取鎖之前,先利用 CAS 將 state 修改為 1,如果成功就將 exclusiveOwnerThread 設(shè)置為當(dāng)前線程。
state 是鎖的標(biāo)志,利用 CAS 將 state 從 0 修改為 1 就代表獲取到了該鎖。
所以非公平鎖和公平鎖的不同之處在于lock()之后,公平鎖直接調(diào)用 acquire()方法,而非公平鎖先利用 CAS 搶鎖,如果 CAS 獲取鎖失敗再調(diào)用 acquire()方法。
那么,非公平鎖先利用 CAS 搶鎖到底有什么作用呢?
回憶一下釋放鎖的過程 AQS.release()方法:
如果在線程 2 在線程 1 釋放鎖的過程中調(diào)用 lock()方法獲取鎖,
對于公平鎖:線程 2 只能先加入同步隊列的隊尾,等隊列中在它之前的線程獲取、釋放鎖之后才有機會去搶鎖。這也就保證了公平,先到先得。
對于非公平鎖:線程 1 釋放鎖過程執(zhí)行到一半,“①state 改為 0,exclusiveOwnerThread 設(shè)置為 null”已經(jīng)完成,此時線程 2 調(diào)用 lock(),那么 CAS 就搶鎖成功。這種情況下線程 2 是可以先獲取非公平鎖而不需要進(jìn)入隊列中排隊的,也就不公平了。
tryAcquire()方法源碼:
// 公平鎖protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {// state==0表示沒有線程占用鎖
if (!hasQueuedPredecessors() && // AQS隊列中沒有結(jié)點時,再去獲取鎖
compareAndSetState(0, acquires)) { // CAS獲取鎖
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;
}
// 非公平鎖
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {// state==0表示沒有線程占用鎖
if (compareAndSetState(0, acquires)) {// CAS獲取鎖
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;
}
兩個 tryAcquire()方法只有一行代碼不同,公平鎖多了一行!hasQueuedPredecessors()。hasQueuedPredecessors()方法是判斷 AQS 隊列中是否還有結(jié)點,如果隊列中沒有結(jié)點返回 false。
公平鎖的 tryAcquire():如果 AQS 同步隊列中仍然有線程在排隊,即使這個時刻沒有線程占用鎖時,當(dāng)前線程也是不能去搶鎖的,這樣可以保證先來等鎖的線程先有機會獲取鎖。
非公平鎖的 tryAcquire():**只要當(dāng)前時刻沒有線程占用鎖,不管同步隊列中是什么情況,當(dāng)前線程都可以去搶鎖。**如果當(dāng)前線程搶到了鎖,對于那些早早在隊列中排隊等鎖的線程就是不公平的了。
分析總結(jié):
非公平鎖和公平鎖只有兩處不同:
公平鎖直接調(diào)用 acquire(),當(dāng)前線程到同步隊列中排隊等鎖。
非公平鎖會先利用 CAS 搶鎖,搶不到鎖才會調(diào)用 acquire()。
公平鎖在同步隊列還有線程等鎖時,即使鎖沒有被占用,也不能獲取鎖。非公平鎖不管同步隊列中是什么情況,直接去搶鎖。
3. 公平鎖 VS 非公平鎖
非公平鎖有可能導(dǎo)致線程永遠(yuǎn)無法獲取到鎖,造成饑餓現(xiàn)象。而公平鎖保證線程獲取鎖的順序符合請求上的時間順序,滿足 FIFO,可以解決饑餓問題。
公平鎖為了保證時間上的絕對順序,需要頻繁的上下文切換,性能開銷較大。而非公平鎖會降低一定的上下文切換,有更好的性能,可以保證更大的吞吐量,這也是 ReentrantLock 默認(rèn)選擇的是非公平鎖的原因。
總結(jié)
一個線程因為得不到 CPU 運行時間,就會處于饑餓狀態(tài)。公平鎖是為了解決饑餓問題。
公平鎖要求線程獲取鎖的順序符合請求上的時間順序,滿足 FIFO。
在獲取公平鎖時,要先看同步隊列中是否有線程在等鎖,如果有線程已經(jīng)在等鎖了,就只能將當(dāng)前線程加到隊尾。只有沒有線程等鎖時才能獲取鎖。而在獲取非公平鎖時,不管同步隊列中是什么情況,只要有機會就嘗試搶鎖。
非公平鎖有更好的性能,可以保證更大的吞吐量。
參考資料
并發(fā)系列文章匯總
【原創(chuàng)】01|開篇獲獎感言
【原創(chuàng)】02|并發(fā)編程三大核心問題
【原創(chuàng)】03|重排序-可見性和有序性問題根源
【原創(chuàng)】04|Java 內(nèi)存模型詳解
【原創(chuàng)】05|深入理解 volatile
【原創(chuàng)】06|你不知道的 final
【原創(chuàng)】07|synchronized 原理
【原創(chuàng)】08|synchronized 鎖優(yōu)化
【原創(chuàng)】09|基礎(chǔ)干貨
【原創(chuàng)】10|線程狀態(tài)
【原創(chuàng)】11|線程調(diào)度
【原創(chuàng)】13|LockSupport
【原創(chuàng)】14|AQS 源碼分析
【原創(chuàng)】15|重入鎖 ReentrantLock
————??e n d?————
金三銀四,師長為大家準(zhǔn)備了三份面試寶典:
《java面試寶典5.0》
《350道Java面試題:整理自100+公司》
《資深java面試寶典-視頻版》
分別適用于初中級,中高級,以及資深級工程師的面試復(fù)習(xí)。
內(nèi)容包含java基礎(chǔ)、javaweb、各個性能優(yōu)化、JVM、鎖、高并發(fā)、反射、Spring原理、微服務(wù)、Zookeeper、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)、限流熔斷降級等等。
獲取方式:點“在看”,V信關(guān)注師長的小號:編程最前線并回復(fù)?面試?領(lǐng)取,更多精彩陸續(xù)奉上。
總結(jié)
以上是生活随笔為你收集整理的reentrantlock非公平锁不会随机挂起线程?_【原创】Java并发编程系列16 | 公平锁与非公平锁...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 小米 POCO X5 Pro 登陆印度市
- 下一篇: Omdia:网速 1Gbps 及以上的联