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