圖解Java?ReentrantLock公平鎖和非公平鎖的實現(xiàn)
概述
ReentrantLock是Java并發(fā)中十分常用的一個類,具備類似synchronized鎖的作用。但是相比synchronized, 它具備更強的能力,同時支持公平鎖和非公平鎖。
公平鎖: 指多個線程按照申請鎖的順序來獲取鎖,線程直接進入隊列中排隊,隊列中的第一個線程才能獲得鎖。
非公平鎖: 多個線程加鎖時直接嘗試獲取鎖,能搶到鎖到直接占有鎖,搶不到才會到等待隊列的隊尾等待。
那ReentrantLock中具體是怎么實現(xiàn)公平和非公鎖的呢?它們之間又有什么優(yōu)缺點呢?本文就帶大家一探究竟。
RenentrantLock原理概述

上面是RenentrantLock的類結構圖。
- RenentrantLock實現(xiàn)了Lock接口,Lock接口提供了鎖的通用api,比如加鎖lock,解鎖unlock等操作。
- RenentrantLock底層加鎖是通過AQS實現(xiàn)的,兩個內部類FairSync服務于公平鎖,NofaireSync服務于非公平鎖的實現(xiàn),他們統(tǒng)一繼承自AQS。
ReentrantLock 類 API:
public void lock():獲得鎖
如果鎖沒有被另一個線程占用,則將鎖定計數(shù)設置為 1
如果當前線程已經(jīng)保持鎖定,則保持計數(shù)增加 1
如果鎖被另一個線程保持,則當前線程被禁用線程調度,并且在鎖定已被獲取之前處于休眠狀態(tài)
public void unlock():嘗試釋放鎖
如果當前線程是該鎖的持有者,則保持計數(shù)遞減
如果保持計數(shù)現(xiàn)在為零,則鎖定被釋放
如果當前線程不是該鎖的持有者,則拋出異常
關于AQS的原理, 強烈大家閱讀深入淺出理解Java并發(fā)AQS的獨占鎖模式
非公平鎖實現(xiàn)
演示
@Test
public void testUnfairLock() throws InterruptedException {
// 無參構造函數(shù),默認創(chuàng)建非公平鎖模式
ReentrantLock reentrantLock = new ReentrantLock();
for (int i = 0; i < 10; i++) {
final int threadNum = i;
new Thread(() -> {
reentrantLock.lock();
try {
System.out.println("線程" + threadNum + "獲取鎖");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// finally中解鎖
reentrantLock.unlock();
System.out.println("線程" + threadNum +"釋放鎖");
}
}).start();
Thread.sleep(999);
}
Thread.sleep(100000);
}運行結果:
線程0獲取鎖
線程0釋放鎖
線程1獲取鎖
線程1釋放鎖
線程3獲取鎖
線程3釋放鎖
線程2獲取鎖
線程2釋放鎖
線程5獲取鎖
線程5釋放鎖
線程4獲取鎖
線程4釋放鎖
....
- 默認構造函數(shù)創(chuàng)建的是非公平鎖
- 運行結果可以看到線程3優(yōu)先于線程2獲取鎖(這個結果是人為造的,很難模擬出來)。
加鎖原理
1.構造函數(shù)創(chuàng)建鎖對象
public ReentrantLock() {
sync = new NonfairSync();
}默認構造函數(shù),創(chuàng)建了NonfairSync,非公平鎖同步器,它是繼承自AQS.
2.第一個線程加鎖時,不存在競爭,如下圖:
// ReentrantLock.NonfairSync#lock
final void lock() {
// 用 cas 嘗試(僅嘗試一次)將 state 從 0 改為 1, 如果成功表示【獲得了獨占鎖】
if (compareAndSetState(0, 1))
// 設置當前線程為獨占線程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);//失敗進入
}
- cas修改state從0到1,獲取鎖
- 設置鎖對象的線程為當前線程
3.第二個線程申請加鎖時,出現(xiàn)鎖競爭,如下圖:

Thread-1 執(zhí)行,CAS 嘗試將 state 由 0 改為 1,結果失?。ǖ谝淮危?,進入 acquire 邏輯
// AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) {
// tryAcquire 嘗試獲取鎖失敗時, 會調用 addWaiter 將當前線程封裝成node入隊,acquireQueued 阻塞當前線程,
// acquireQueued 返回 true 表示掛起過程中線程被中斷喚醒過,false 表示未被中斷過
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如果線程被中斷了邏輯來到這,完成一次真正的打斷效果
selfInterrupt();
}調用tryAcquire方法嘗試獲取鎖,這里由子類NonfairSync實現(xiàn)。
如果tryAcquire獲取鎖失敗,通過addWaiter方法將當前線程封裝成節(jié)點,入隊
acquireQueued方法會將當前線程阻塞
// ReentrantLock.NonfairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
// 搶占成功返回 true,搶占失敗返回 false
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// state 值
int c = getState();
// 條件成立說明當前處于【無鎖狀態(tài)】
if (c == 0) {
//如果還沒有獲得鎖,嘗試用cas獲得,這里體現(xiàn)非公平性: 不去檢查 AQS 隊列是否有阻塞線程直接獲取鎖
if (compareAndSetState(0, acquires)) {
// 獲取鎖成功設置當前線程為獨占鎖線程。
setExclusiveOwnerThread(current);
return true;
}
}
// 這部分是重入鎖的原理
// 如果已經(jīng)有線程獲得了鎖, 獨占鎖線程還是當前線程, 表示【發(fā)生了鎖重入】
else if (current == getExclusiveOwnerThread()) {
// 更新鎖重入的值
int nextc = c + acquires;
// 越界判斷,當重入的深度很深時,會導致 nextc < 0,int值達到最大之后再 + 1 變負數(shù)
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 更新 state 的值,這里不使用 cas 是因為當前線程正在持有鎖,所以這里的操作相當于在一個管程內
setState(nextc);
return true;
}
// 獲取失敗
return false;
}正是這個方法體現(xiàn)了非公平鎖,在nonfairTryAcquire如果發(fā)現(xiàn)state=0,無鎖的情況,它會忽略隊列中等待的線程,優(yōu)先獲取一次鎖,相當于"插隊"。
4.第二個線程tryAcquire申請鎖失敗,通過執(zhí)行addWaiter方法加入到隊列中。

- 圖中黃色三角表示該 Node 的 waitStatus 狀態(tài),其中 0 為默認正常狀態(tài)
- Node 的創(chuàng)建是懶惰的,其中第一個 Node 稱為 Dummy(啞元)或哨兵,用來占位,并不關聯(lián)線程。
// AbstractQueuedSynchronizer#addWaiter,返回當前線程的 node 節(jié)點
private Node addWaiter(Node mode) {
// 將當前線程關聯(lián)到一個 Node 對象上, 模式為獨占模式
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 快速入隊,如果 tail 不為 null,說明存在阻塞隊列
if (pred != null) {
// 將當前節(jié)點的前驅節(jié)點指向 尾節(jié)點
node.prev = pred;
// 通過 cas 將 Node 對象加入 AQS 隊列,成為尾節(jié)點,【尾插法】
if (compareAndSetTail(pred, node)) {
pred.next = node;// 雙向鏈表
return node;
}
}
// 初始時隊列為空,或者 CAS 失敗進入這里
enq(node);
return node;
}// AbstractQueuedSynchronizer#enq
private Node enq(final Node node) {
// 自旋入隊,必須入隊成功才結束循環(huán)
for (;;) {
Node t = tail;
// 說明當前鎖被占用,且當前線程可能是【第一個獲取鎖失敗】的線程,【還沒有建立隊列】
if (t == null) {
// 設置一個【啞元節(jié)點】,頭尾指針都指向該節(jié)點
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 自旋到這,普通入隊方式,首先賦值尾節(jié)點的前驅節(jié)點【尾插法】
node.prev = t;
// 【在設置完尾節(jié)點后,才更新的原始尾節(jié)點的后繼節(jié)點,所以此時從前往后遍歷會丟失尾節(jié)點】
if (compareAndSetTail(t, node)) {
//【此時 t.next = null,并且這里已經(jīng) CAS 結束,線程并不是安全的】
t.next = node;
return t; // 返回當前 node 的前驅節(jié)點
}
}
}
}5.第二個線程加入隊列后,現(xiàn)在要做的是想辦法阻塞線程,不讓它執(zhí)行,就看acquireQueued的了。

- 圖中黃色三角表示該 Node 的 waitStatus 狀態(tài),0 為默認正常狀態(tài), 但是-1狀態(tài)表示它肩負喚醒下一個節(jié)點的線程。
- 灰色表示線程阻塞了。
inal boolean acquireQueued(final Node node, int arg) {
// true 表示當前線程搶占鎖失敗,false 表示成功
boolean failed = true;
try {
// 中斷標記,表示當前線程是否被中斷
boolean interrupted = false;
for (;;) {
// 獲得當前線程節(jié)點的前驅節(jié)點
final Node p = node.predecessor();
// 前驅節(jié)點是 head, FIFO 隊列的特性表示輪到當前線程可以去獲取鎖
if (p == head && tryAcquire(arg)) {
// 獲取成功, 設置當前線程自己的 node 為 head
setHead(node);
p.next = null; // help GC
// 表示搶占鎖成功
failed = false;
// 返回當前線程是否被中斷
return interrupted;
}
// 判斷是否應當 park,返回 false 后需要新一輪的循環(huán),返回 true 進入條件二阻塞線程
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
// 條件二返回結果是當前線程是否被打斷,沒有被打斷返回 false 不進入這里的邏輯
// 【就算被打斷了,也會繼續(xù)循環(huán),并不會返回】
interrupted = true;
}
} finally {
// 【可打斷模式下才會進入該邏輯】
if (failed)
cancelAcquire(node);
}
}- acquireQueued 會在一個自旋中不斷嘗試獲得鎖,失敗后進入 park 阻塞
- 如果當前線程是在 head 節(jié)點后,也就是第一個節(jié)點,又會直接多一次機會 tryAcquire 嘗試獲取鎖,如果還是被占用,會返回失敗。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 表示前置節(jié)點是個可以喚醒當前節(jié)點的節(jié)點,返回 true
if (ws == Node.SIGNAL)
return true;
// 前置節(jié)點的狀態(tài)處于取消狀態(tài),需要【刪除前面所有取消的節(jié)點】, 返回到外層循環(huán)重試
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
// 獲取到非取消的節(jié)點,連接上當前節(jié)點
pred.next = node;
// 默認情況下 node 的 waitStatus 是 0,進入這里的邏輯
} else {
// 【設置上一個節(jié)點狀態(tài)為 Node.SIGNAL】,返回外層循環(huán)重試
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 返回不應該 park,再次嘗試一次
return false;
}- shouldParkAfterFailedAcquire發(fā)現(xiàn)前驅節(jié)點等待狀態(tài)是-1, 返回true,表示需要阻塞。
- shouldParkAfterFailedAcquire發(fā)現(xiàn)前驅節(jié)點等待狀態(tài)大于0,說明是無效節(jié)點,會進行清理。
- shouldParkAfterFailedAcquire發(fā)現(xiàn)前驅節(jié)點等待狀態(tài)等于0,將前驅 node 的 waitStatus 改為 -1,返回 false。
private final boolean parkAndCheckInterrupt() {
// 阻塞當前線程,如果打斷標記已經(jīng)是 true, 則 park 會失效
LockSupport.park(this);
// 判斷當前線程是否被打斷,清除打斷標記
return Thread.interrupted();
}- 通過不斷自旋嘗試獲取鎖,最終前驅節(jié)點的等待狀態(tài)為-1的時候,進行阻塞當前線程。
- 通過調用LockSupport.park方法進行阻塞。
6.多個線程嘗試獲取鎖,競爭失敗后,最終形成下面的圖形。

釋放鎖原理
1.第一個線程通過調用unlock方法釋放鎖。
public void unlock() {
sync.release(1);
}最終調用的是同步器的release方法。

設置鎖定的線程exclusiveOwnerThread為null
設置鎖的state為0
// AbstractQueuedSynchronizer#release
public final boolean release(int arg) {
// 嘗試釋放鎖,tryRelease 返回 true 表示當前線程已經(jīng)【完全釋放鎖,重入的釋放了】
if (tryRelease(arg)) {
// 隊列頭節(jié)點
Node h = head;
// 頭節(jié)點什么時候是空?沒有發(fā)生鎖競爭,沒有競爭線程創(chuàng)建啞元節(jié)點
// 條件成立說明阻塞隊列有等待線程,需要喚醒 head 節(jié)點后面的線程
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}進入 tryRelease,設置 exclusiveOwnerThread 為 null,state = 0
當前隊列不為 null,并且 head 的 waitStatus = -1,進入 unparkSuccessor, 喚醒阻塞的線程
2.線程一通過調用tryRelease方法釋放鎖,該類的實現(xiàn)是在子類中
// ReentrantLock.Sync#tryRelease
protected final boolean tryRelease(int releases) {
// 減去釋放的值,可能重入
int c = getState() - releases;
// 如果當前線程不是持有鎖的線程直接報錯
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 是否已經(jīng)完全釋放鎖
boolean free = false;
// 支持鎖重入, 只有 state 減為 0, 才完全釋放鎖成功
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 當前線程就是持有鎖線程,所以可以直接更新鎖,不需要使用 CAS
setState(c);
return free;
}修改鎖資源的state
3.喚醒隊列中第一個線程Thread1
private void unparkSuccessor(Node node) {
// 當前節(jié)點的狀態(tài)
int ws = node.waitStatus;
if (ws < 0)
// 【嘗試重置狀態(tài)為 0】,因為當前節(jié)點要完成對后續(xù)節(jié)點的喚醒任務了,不需要 -1 了
compareAndSetWaitStatus(node, ws, 0);
// 找到需要 unpark 的節(jié)點,當前節(jié)點的下一個
Node s = node.next;
// 已取消的節(jié)點不能喚醒,需要找到距離頭節(jié)點最近的非取消的節(jié)點
if (s == null || s.waitStatus > 0) {
s = null;
// AQS 隊列【從后至前】找需要 unpark 的節(jié)點,直到 t == 當前的 node 為止,找不到就不喚醒了
for (Node t = tail; t != null && t != node; t = t.prev)
// 說明當前線程狀態(tài)需要被喚醒
if (t.waitStatus <= 0)
// 置換引用
s = t;
}
// 【找到合適的可以被喚醒的 node,則喚醒線程】
if (s != null)
LockSupport.unpark(s.thread);
}- 從后往前找到隊列中距離 head 最近的一個沒取消的 Node,unpark 恢復其運行,本例中即為 Thread-1
- thread1活了,開始重新去獲取鎖,也就是前面acquireQueued中的流程。
為什么這里查找喚醒的節(jié)點是從后往前,而不是從前往后呢?
從后向前的喚醒的原因:enq 方法中,節(jié)點是尾插法,首先賦值的是尾節(jié)點的前驅節(jié)點,此時前驅節(jié)點的 next 并沒有指向尾節(jié)點,從前遍歷會丟失尾節(jié)點。
4.Thread1恢復執(zhí)行流程

- 喚醒的Thread-1 線程會從 park 位置開始執(zhí)行,如果加鎖成功(沒有競爭),設置了exclusiveOwnerThread為Thread-1, state=1。
- head 指向剛剛 Thread-1 所在的 Node,該 Node 會清空 Thread
- 原本的 head 因為從鏈表斷開,而可被垃圾回收

5.另一種可能,突然來了Thread-4來競爭,體現(xiàn)非公平鎖
如果這時有其它線程來競爭鎖,例如這時有 Thread-4 來了并搶占了鎖,很有可能搶占成功。

Thread-4 被設置為 exclusiveOwnerThread,state = 1
Thread-1 再次進入 acquireQueued 流程,獲取鎖失敗,重新進入 park 阻塞
公平鎖實現(xiàn)
演示
@Test
public void testfairLock() throws InterruptedException {
// 有參構造函數(shù),true表示公平鎖,false表示非公平鎖
ReentrantLock reentrantLock = new ReentrantLock(true);
for (int i = 0; i < 10; i++) {
final int threadNum = i;
new Thread(() -> {
reentrantLock.lock();
try {
System.out.println("線程" + threadNum + "獲取鎖");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// finally中解鎖
reentrantLock.unlock();
System.out.println("線程" + threadNum +"釋放鎖");
}
}).start();
Thread.sleep(10);
}
Thread.sleep(100000);
}運行結果:
線程0獲取鎖
線程0釋放鎖
線程1獲取鎖
線程1釋放鎖
線程2獲取鎖
線程2釋放鎖
線程3獲取鎖
線程3釋放鎖
線程4獲取鎖
線程4釋放鎖
線程5獲取鎖
線程5釋放鎖
線程6獲取鎖
線程6釋放鎖
線程7獲取鎖
線程7釋放鎖
線程8獲取鎖
線程8釋放鎖
線程9獲取鎖
線程9釋放鎖
ReentrantLock有參構造函數(shù),true表示公平鎖,false表示非公平鎖
觀察運行結果,所有獲取鎖的過程都是根據(jù)申請鎖的時間保持一致。
原理實現(xiàn)
公平鎖和非公鎖的整體流程基本是一致的,唯一不同的是嘗試獲取鎖tryAcquire的實現(xiàn)。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 先檢查 AQS 隊列中是否有前驅節(jié)點, 沒有(false)才去競爭
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 鎖重入
return false;
}
}public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// 頭尾指向一個節(jié)點,鏈表為空,返回false
return h != t &&
// 頭尾之間有節(jié)點,判斷頭節(jié)點的下一個是不是空
// 不是空進入最后的判斷,第二個節(jié)點的線程是否是本線程,不是返回 true,表示當前節(jié)點有前驅節(jié)點
((s = h.next) == null || s.thread != Thread.currentThread());
}與非公平鎖最大的區(qū)別是:公平鎖獲取鎖的時候先檢查 AQS 隊列中是否有非當前線程的等待節(jié)點,沒有才去 CAS 競爭,有的話,就老老實實排隊去吧。而非公平鎖會嘗試搶一次鎖,如果搶不到的話,老老實實排隊去吧。
總結
非公平鎖和公平鎖的兩處不同:
- 非公平鎖在調用 lock 后,首先就會調用 CAS 進行一次搶鎖,如果這個時候恰巧鎖沒有被占用,那么直接就獲取到鎖返回了。
- 非公平鎖在 CAS 失敗后,和公平鎖一樣都會進入到 tryAcquire 方法,在 tryAcquire 方法中,如果發(fā)現(xiàn)鎖這個時候被釋放了(state == 0),非公平鎖會直接 CAS 搶鎖,但是公平鎖會判斷等待隊列是否有線程處于等待狀態(tài),如果有則不去搶鎖,乖乖排到后面。
公平鎖和非公平鎖就這兩點區(qū)別,如果這兩次 CAS 都不成功,那么后面非公平鎖和公平鎖是一樣的,都要進入到阻塞隊列等待喚醒。
相對來說,非公平鎖會有更好的性能,因為它的吞吐量比較大。當然,非公平鎖讓獲取鎖的時間變得更加不確定,可能會導致在阻塞隊列中的線程長期處于饑餓狀態(tài)。
以上就是圖解Java ReentrantLock公平鎖和非公平鎖的實現(xiàn)的詳細內容,更多關于Java ReentrantLock公平鎖 非公平鎖的資料請關注腳本之家其它相關文章!
相關文章
Mybatis控制臺打印SQL語句的兩種實現(xiàn)方式
在使用Mybatis開發(fā)時,由于可以動態(tài)拼接SQL,當動態(tài)SQL拼接塊過多,直接從*mapper.xml中找出完整的SQL較難,此時,可以通過兩種方法調試出SQL,方法一,將ibatislog4j運行級別調到DEBUG,在控制臺打印出ibatis運行的SQL語句2024-10-10
解決SpringBoot加載application.properties配置文件的坑
這篇文章主要介紹了SpringBoot加載application.properties配置文件的坑,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08
SpringBoot服務設置禁止server.point端口的使用
本文主要介紹了SpringBoot服務設置禁止server.point端口的使用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2024-01-01
Struts2攔截器Interceptor的原理與配置實例詳解
攔截器是一種AOP(面向切面編程)思想的編程方式.它提供一種機制是開發(fā)者能夠把相對獨立的代碼抽離出來,配置到Action前后執(zhí)行。下面這篇文章主要給大家介紹了關于Struts2攔截器Interceptor的原理與配置的相關資料,需要的朋友可以參考下。2017-11-11

