在面試肠套,很多時(shí)間面試官都會(huì)問到鎖的問題舰涌,ReentrantLock也是常問一個(gè)點(diǎn),但具體會(huì)問什么呢?在網(wǎng)上收集到一些問題:
重入鎖是什么你稚?
公平鎖和非公平鎖是什么瓷耙?有什么區(qū)別?
ReentrantLock::lock公平鎖模式現(xiàn)實(shí)
ReentrantLock如何實(shí)現(xiàn)公平鎖刁赖?
ReentrantLock如何實(shí)現(xiàn)可重入搁痛?
ReentrantLock公平鎖模式與非公平鎖獲取鎖的區(qū)別?
ReentrantLock::unlock()釋放鎖,如何喚醒等待隊(duì)列中的線程宇弛?
ReentrantLock除了可重入還有哪些特性鸡典?
ReentrantLock與Synchrionized的區(qū)別
ReentrantLock使用場(chǎng)景
那么重入鎖是什么?有什么用呢
ReentrantLock是什么枪芒?
ReentrantLock是個(gè)典型的獨(dú)占模式AQS彻况,同步狀態(tài)為0時(shí)表示空閑。當(dāng)有線程獲取到空閑的同步狀態(tài)時(shí)舅踪,它會(huì)將同步狀態(tài)加1纽甘,將同步狀態(tài)改為非空閑,于是其他線程掛起等待硫朦。在修改同步狀態(tài)的同時(shí)贷腕,并記錄下自己的線程,作為后續(xù)重入的依據(jù)咬展,即一個(gè)線程持有某個(gè)對(duì)象的鎖時(shí)泽裳,再次去獲取這個(gè)對(duì)象的鎖是可以成功的。如果是不可重入的鎖的話破婆,就會(huì)造成死鎖涮总。
ReentrantLock會(huì)涉及到公平鎖和非公平鎖,實(shí)現(xiàn)關(guān)鍵在于成員變量 sync
的實(shí)現(xiàn)不同,這是鎖實(shí)現(xiàn)互斥同步的核心祷舀。
//公平鎖和非公平鎖的變量
private final Sync sync;
//父類
abstract static class Sync extends AbstractQueuedSynchronizer {}
//公平鎖子類
static final class FairSync extends Sync {}
//非公平鎖子類
static final class NonfairSync extends Sync {}
那公平鎖和非公平鎖是什么瀑梗?有什么區(qū)別烹笔?
那公平鎖和非公平鎖是什么?有什么區(qū)別抛丽?
公平鎖是指當(dāng)鎖可用時(shí),在鎖上等待時(shí)間最長(zhǎng)的線程將獲得鎖的使用權(quán)谤职,即先進(jìn)先出。而非公平鎖則隨機(jī)分配這種使用權(quán)亿鲜,是一種搶占機(jī)制允蜈,是隨機(jī)獲得鎖,并不是先來的一定能先得到鎖蒿柳。
ReentrantLock提供了一個(gè)構(gòu)造方法饶套,可以實(shí)現(xiàn)公平鎖或非公平鎖:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
雖然公平鎖在公平性得以保障,但因?yàn)楣降墨@取鎖沒有考慮到操作系統(tǒng)對(duì)線程的調(diào)度因素以及其他因素垒探,會(huì)影響性能妓蛮。
雖然非公平模式效率比較高,但是非公平模式在申請(qǐng)獲取鎖的線程足夠多,那么可能會(huì)造成某些線程長(zhǎng)時(shí)間得不到鎖圾叼,這就是非公平鎖的“饑餓”問題蛤克。
但大部分情況下我們使用非公平鎖,因?yàn)槠湫阅鼙裙芥i好很多褐奥。但是公平鎖能夠避免線程饑餓咖耘,某些情況下也很有用。
接下來看看ReentrantLock公平鎖的實(shí)現(xiàn):
ReentrantLock::lock公平鎖模式實(shí)現(xiàn)
首先需要在構(gòu)建函數(shù)中傳入 true
創(chuàng)建好公平鎖
ReentrantLock reentrantLock = new ReentrantLock(true);
調(diào)用 lock()
進(jìn)行上鎖撬码,直接 acquire(1)
上鎖
public void lock() {
// 調(diào)用的sync的子類FairSync的lock()方法:ReentrantLock.FairSync.lock()
sync.lock();
}
final void lock() {
// 調(diào)用AQS的acquire()方法獲取鎖儿倒,傳的值為1
acquire(1);
}
直接嘗試獲取鎖,
// AbstractQueuedSynchronizer.acquire()
public final void acquire(int arg) {
// 嘗試獲取鎖
// 如果失敗了呜笑,就排隊(duì)
if (!tryAcquire(arg) &&
// 注意addWaiter()這里傳入的節(jié)點(diǎn)模式為獨(dú)占模式
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
具體獲取鎖流程
-
getState()
獲取同步狀態(tài)state
****值夫否,進(jìn)行判斷是否為0 :image 如果狀態(tài)變量的值為0,說明暫時(shí)還沒有人占有鎖叫胁, 使用hasQueuedPredecessors()保證了不論是新的線程還是已經(jīng)排隊(duì)的線程都順序使用鎖凰慈,如果沒有其它線程在排隊(duì),那么當(dāng)前線程嘗試更新state的值為1驼鹅,并自己設(shè)置到exclusiveOwnerThread變量中微谓,供后續(xù)自己可重入獲取鎖作準(zhǔn)備。
如果exclusiveOwnerThread中為當(dāng)前線程說明本身就占有著鎖输钩,現(xiàn)在又嘗試獲取鎖豺型,需要將狀態(tài)變量的值
state+1
// ReentrantLock.FairSync.tryAcquire()
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 狀態(tài)變量的值為0,說明暫時(shí)還沒有線程占有鎖
if (c == 0) {
// hasQueuedPredecessors()保證了不論是新的線程還是已經(jīng)排隊(duì)的線程都順序使用鎖
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 當(dāng)前線程獲取了鎖买乃,并將本線程設(shè)置到exclusiveOwnerThread變量中姻氨,
//供后續(xù)自己可重入獲取鎖作準(zhǔn)備
setExclusiveOwnerThread(current);
return true;
}
}
// 之所以說是重入鎖,就是因?yàn)樵讷@取鎖失敗的情況下剪验,還會(huì)再次判斷是否當(dāng)前線程已經(jīng)持有鎖了
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 設(shè)置到state中
// 因?yàn)楫?dāng)前線程占有著鎖肴焊,其它線程只會(huì)CAS把state從0更新成1前联,是不會(huì)成功的
// 所以不存在競(jìng)爭(zhēng),自然不需要使用CAS來更新
setState(nextc);
return true;
}
return false;
}
如果獲取失敗加入隊(duì)列里娶眷,那具體怎么處理呢似嗤?通過自旋的方式,隊(duì)列中線程不斷進(jìn)行嘗試獲取鎖操作届宠,中間是可以通過中斷的方式打斷双谆,
- 如果當(dāng)前節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)為head節(jié)點(diǎn),則說明輪到自己獲取鎖了席揽,調(diào)用
tryAcquire()
方法再次嘗試獲取鎖
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 自旋
for (;;) {
// 當(dāng)前節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn),
final Node p = node.predecessor();
// 如果當(dāng)前節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)為head節(jié)點(diǎn)谓厘,則說明輪到自己獲取鎖了
// 調(diào)用ReentrantLock.FairSync.tryAcquire()方法再次嘗試獲取鎖
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
// 未失敗
failed = false;
return interrupted;
}
// 是否需要阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
// 如果失敗了幌羞,取消獲取鎖
cancelAcquire(node);
}
}
- 當(dāng)前的Node的上一個(gè)節(jié)點(diǎn)不是Head,是需要判斷是否需要阻塞,以及尋找安全點(diǎn)掛起竟稳。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 上一個(gè)節(jié)點(diǎn)的等待狀態(tài)
int ws = pred.waitStatus;
// 等待狀態(tài)為SIGNAL(等待喚醒)属桦,直接返回true
if (ws == Node.SIGNAL)
return true;
// 前一個(gè)節(jié)點(diǎn)的狀態(tài)大于0,已取消狀態(tài)
if (ws > 0) {
// 把前面所有取消狀態(tài)的節(jié)點(diǎn)都從鏈表中刪除
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 前一個(gè)Node的狀態(tài)小于等于0他爸,則把其狀態(tài)設(shè)置為等待喚醒
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
在看完獲取鎖的流程聂宾,那么你知道ReentrantLock如何實(shí)現(xiàn)公平鎖了嗎?其實(shí)就是在 tryAcquire()
的實(shí)現(xiàn)中诊笤。
ReentrantLock如何實(shí)現(xiàn)公平鎖系谐?
在 tryAcquire()
的實(shí)現(xiàn)中使用了 hasQueuedPredecessors()
保證了線程先進(jìn)先出FIFO的使用鎖,不會(huì)產(chǎn)生"饑餓"問題讨跟,
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 狀態(tài)變量的值為0纪他,說明暫時(shí)還沒有線程占有鎖
if (c == 0) {
// hasQueuedPredecessors()保證了不論是新的線程還是已經(jīng)排隊(duì)的線程都順序使用鎖
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
....
}
...
}
}
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
tryAcquire都會(huì)檢查CLH隊(duì)列中是否仍有前驅(qū)的元素,如果仍然有那么繼續(xù)等待晾匠,通過這種方式來保證先來先服務(wù)的原則茶袒。
那這樣ReentrantLock如何實(shí)現(xiàn)可重入?是怎么重入的凉馆?
ReentrantLock如何實(shí)現(xiàn)可重入薪寓?
其實(shí)也很簡(jiǎn)單,在獲取鎖后澜共,設(shè)置一個(gè)標(biāo)識(shí)變量為當(dāng)前線程 exclusiveOwnerThread
向叉,當(dāng)線程再次進(jìn)入判斷 exclusiveOwnerThread
變量是否等于本線程來判斷.
protected final boolean tryAcquire(int acquires) {
// 狀態(tài)變量的值為0,說明暫時(shí)還沒有線程占有鎖
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 當(dāng)前線程獲取了鎖咳胃,并將本線程設(shè)置到exclusiveOwnerThread變量中植康,
//供后續(xù)自己可重入獲取鎖作準(zhǔn)備
setExclusiveOwnerThread(current);
return true;
}
} //之所以說是重入鎖,就是因?yàn)樵讷@取鎖失敗的情況下展懈,還會(huì)再次判斷是否當(dāng)前線程已經(jīng)持有鎖了
else if (current == getExclusiveOwnerThread()) {
...
}
}
當(dāng)看完公平鎖獲取鎖的流程销睁,那其實(shí)我們也了解非公平鎖獲取鎖供璧,那我們來看看。
ReentrantLock公平鎖模式與非公平鎖獲取鎖的區(qū)別冻记?
其實(shí)非公平鎖獲取鎖獲取區(qū)別主要在于:
構(gòu)建函數(shù)中傳入
false
或者為null睡毒,為創(chuàng)建非公平鎖NonfairSync
,true
創(chuàng)建公平鎖,非公平鎖在獲取鎖的時(shí)候冗栗,先去檢查
state
狀態(tài)演顾,再直接執(zhí)行aqcuire(1)
,這樣可以提高效率隅居,
final void lock() {
if (compareAndSetState(0, 1))
//修改同步狀態(tài)的值成功的話钠至,設(shè)置當(dāng)前線程為獨(dú)占的線程
setExclusiveOwnerThread(Thread.currentThread());
else
//獲取鎖
acquire(1);
}
- 在
tryAcquire()
中沒有hasQueuedPredecessors()
保證了不論是新的線程還是已經(jīng)排隊(duì)的線程都順序使用鎖。
其他功能都類似胎源。在理解了獲取鎖下棉钧,我們更好理解ReentrantLock::unlock()鎖的釋放,也比較簡(jiǎn)單涕蚤。
ReentrantLock::unlock()釋放鎖宪卿,如何喚醒等待隊(duì)列中的線程?
- 釋放當(dāng)前線程占用的鎖
protected final boolean tryRelease(int releases) {
// 計(jì)算釋放后state值
int c = getState() - releases;
// 如果不是當(dāng)前線程占用鎖,那么拋出異常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
// 鎖被重入次數(shù)為0,表示釋放成功
free = true;
// 清空獨(dú)占線程
setExclusiveOwnerThread(null);
}
// 更新state值
setState(c);
return free;
}
若釋放成功万栅,就需要喚醒等待隊(duì)列中的線程 佑钾,先查看頭結(jié)點(diǎn)的狀態(tài)是否為SIGNAL,如果是則喚醒頭結(jié)點(diǎn)的下個(gè)節(jié)點(diǎn)關(guān)聯(lián)的線程烦粒,如果釋放失敗那么返回false表示解鎖失敗休溶。
設(shè)置waitStatus為0,
當(dāng)頭結(jié)點(diǎn)下一個(gè)節(jié)點(diǎn)不為空的時(shí)候扰她,會(huì)直接喚醒該節(jié)點(diǎn)邮偎,如果該節(jié)點(diǎn)為空,則會(huì)隊(duì)尾開始向前遍歷义黎,找到最后一個(gè)不為空的節(jié)點(diǎn)禾进,然后喚醒。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;//這里的s是頭節(jié)點(diǎn)(現(xiàn)在是頭節(jié)點(diǎn)持有鎖)的下一個(gè)節(jié)點(diǎn)廉涕,也就是期望喚醒的節(jié)點(diǎn)
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); //喚醒s代表的線程
}
綜合上面的ReentrantLock的可重入泻云,可實(shí)現(xiàn)公平\非公平鎖的特性外,還具有哪些特性狐蜕?
ReentrantLock除了可重入還有哪些特性宠纯?
支持線程中斷,只是在線程上增加一個(gè)中斷標(biāo)志
interrupted
层释,并不會(huì)對(duì)運(yùn)行中的線程有什么影響婆瓜,具體需要根據(jù)這個(gè)中斷標(biāo)志干些什么,用戶自己去決定。比如廉白,實(shí)現(xiàn)了等待鎖的時(shí)候个初,5秒沒有獲取到鎖,中斷等待猴蹂,線程繼續(xù)做其它事情院溺。超時(shí)機(jī)制,在
ReetrantLock::tryLock(long timeout, TimeUnit unit)
提供了超時(shí)獲取鎖的功能磅轻。它的語義是在指定的時(shí)間內(nèi)如果獲取到鎖就返回true珍逸,獲取不到則返回false。這種機(jī)制避免了線程無限期的等待鎖釋放聋溜。
ReentrantLock與Synchrionized的區(qū)別
ReentrantLock支持等待可中斷谆膳,可以中斷等待中的線程
ReentrantLock可實(shí)現(xiàn)公平鎖
ReentrantLock可實(shí)現(xiàn)選擇性通知,即可以有多個(gè)Condition隊(duì)列
ReentrantLock使用場(chǎng)景
場(chǎng)景1:如果已加鎖撮躁,則不再重復(fù)加鎖摹量,多用于進(jìn)行非重要任務(wù)防止重復(fù)執(zhí)行,如馒胆,清除無用臨時(shí)文件,檢查某些資源的可用性凝果,數(shù)據(jù)備份操作等
場(chǎng)景2:如果發(fā)現(xiàn)該操作已經(jīng)在執(zhí)行祝迂,則嘗試等待一段時(shí)間,等待超時(shí)則不執(zhí)行器净,防止由于資源處理不當(dāng)長(zhǎng)時(shí)間占用導(dǎo)致死鎖情況
場(chǎng)景3:如果發(fā)現(xiàn)該操作已經(jīng)加鎖,則等待一個(gè)一個(gè)加鎖,主要用于對(duì)資源的爭(zhēng)搶(如:文件操作逞刷,同步消息發(fā)送冰木,有狀態(tài)的操作等)
場(chǎng)景4:可中斷鎖,取消正在同步運(yùn)行的操作浪慌,來防止不正常操作長(zhǎng)時(shí)間占用造成的阻塞