淺析Java?ReentrantLock鎖的原理與使用
一. AQS內(nèi)部結(jié)構(gòu)介紹
JUC是Java中一個包 java.util.concurrent 。在這個包下,基本存放了Java中一些有關(guān)并發(fā)的類,包括并發(fā)工具,并發(fā)集合,鎖等。
AQS(抽象隊列同步器)是JUC下的一個基礎(chǔ)類,大多數(shù)的并發(fā)工具都是基于AQS實現(xiàn)的。
AQS本質(zhì)并沒有實現(xiàn)太多的業(yè)務(wù)功能,只是對外提供了三點核心內(nèi)容,來幫助實現(xiàn)其他的并發(fā)內(nèi)容。
三點核心內(nèi)容:
int state
- 比如ReentrantLock或者ReentrantReadWriteLock, 它們獲取鎖的方式,都是對state變量做修改實現(xiàn)的。
- 比如CountDownLatch基于state作為計數(shù)器,同樣的Semaphore也是用state記錄資源個數(shù)。
Node對象組成的雙向鏈表(AQS中)
比如ReentrantLock,有一個線程沒有拿到鎖資源,當(dāng)線程需要等待,則需要將線程封裝為Node對象,將Node添加到雙向鏈表,將線程掛起,等待即可。
Node對象組成的單向鏈表(AQS中的ConditionObject類中)
比如ReentrantLock,一個線程持有鎖資源時,執(zhí)行了await方法(類比synchronized鎖執(zhí)行對象的wait方法),此時這個線程需要封裝為Node對象,并添加到單向鏈表。
二. Lock鎖和AQS關(guān)系
ReentrantLock就是基于AQS實現(xiàn)的。ReentrantLock類中維護(hù)這個一個內(nèi)部抽象類Sync,他繼承了AQS類。ReentrantLock的lock和unlock方法就是調(diào)用的Sync的方法。
AQS流程(簡述)
1. 當(dāng)new了一個ReentrantLock時,AQS默認(rèn)state值為0, head 和 tail 都為null;
2. A線程執(zhí)行l(wèi)ock方法,獲取鎖資源。
3. A線程將state通過cas操作從0改為1,代表獲取鎖資源成功。
4. B線程要獲取鎖資源時,鎖資源被A線程持有。
5. B線程獲取鎖資源失敗,需要添加到雙向鏈表中排隊。
6. 掛起B(yǎng)線程,等待A線程釋放鎖資源,再喚醒掛起的B線程。
7. A線程釋放鎖資源,將state從1改為0,再喚醒head.next節(jié)點。
8. B線程就可以重新嘗試獲取鎖資源。
注: 修改AQS雙向鏈表時要保證一個私有屬性變化和兩個共有屬性變化,只需要讓tail變化保證原子性即可。不能先改tail(會破壞雙向鏈表)

三. AQS - Lock鎖的tryAcquire方法
ReentrantLock中的lock方法實際是執(zhí)行的Sync的lock方法。
Sync是一個抽象類,繼承了AQS
Sync有兩個子類實現(xiàn):
- FairSync: 公平鎖
- NonFairSync: 非公平鎖
Sync的lock方法實現(xiàn):
// 非公平鎖
final void lock() {
// CAS操作,嘗試將state從0改為1
// 成功就拿到鎖資源, 失敗執(zhí)行acquire方法
if (compareAndSetState(0, 1))
// 成功就設(shè)置互斥鎖的為當(dāng)前線程擁有
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// 公平鎖
final void lock() {
acquire(1);
}如果CAS操作沒有成功,需要執(zhí)行acquire方法走后續(xù)
acquire方法是AQS提供的,公平和非公平都是走的這個方法
public final void acquire(int arg) {
// 1. tryAcquire方法: 再次嘗試拿鎖
// 2. addWaiter方法: 沒有獲取到鎖資源,去排隊
// 3. acquireQueued方法:掛起線程和后續(xù)被喚醒繼續(xù)獲取鎖資源的邏輯
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如果這個過程中出現(xiàn)中斷,在整個過程結(jié)束后再自我中斷
selfInterrupt();
}在AQS中tryAcquire是沒有具體實現(xiàn)邏輯的,AQS直接在tryAcquire方法中拋出異常
在公平鎖和非公平鎖中有自己的實現(xiàn)。
非公平鎖tryAcquire方法
// 非公平鎖
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
// 非公平鎖再次嘗試拿鎖 (注:該方法屬于Sync類中)
final boolean nonfairTryAcquire(int acquires) {
// 獲取當(dāng)前線程對象
final Thread current = Thread.currentThread();
// 獲取state狀態(tài)
int c = getState();
// state是不是沒有線程持有鎖資源,可以嘗試獲取鎖
if (c == 0) {
// 再次CAS操作嘗試修改state狀態(tài)從0改為1
if (compareAndSetState(0, acquires)) {
// 成功就設(shè)置互斥鎖的為當(dāng)前線程擁有
setExclusiveOwnerThread(current);
return true;
}
}
// 鎖資源是否被當(dāng)前線程所持有 (可重入鎖)
else if (current == getExclusiveOwnerThread()) {
// 持有鎖資源為當(dāng)前, 則對state + 1
int nextc = c + acquires;
// 健壯性判斷
if (nextc < 0) // overflow
// 超過最大鎖重入次數(shù)會拋異常(幾率很小,理論上存在)
throw new Error("Maximum lock count exceeded");
// 設(shè)置state狀態(tài),代表鎖重入成功
setState(nextc);
return true;
}
return false;
}公平鎖tryAcquire方法
// 公平鎖
protected final boolean tryAcquire(int acquires) {
// 獲取當(dāng)前線程對象
final Thread current = Thread.currentThread();
// 獲取state狀態(tài)
int c = getState();
// state是不是沒有線程持有鎖資源
if (c == 0) {
// 當(dāng)前鎖資源沒有被其他線程持有
// hasQueuedPredecessors方法: 鎖資源沒有被持有,進(jìn)入隊列排隊
// 排隊規(guī)則:
// 1. 檢查隊列沒有線程排隊,搶鎖。
// 2. 檢查隊列有線程排隊,查看當(dāng)前線程是否排在第一位,如果是搶鎖,否則入隊列(注:該方法只是判斷,沒有真正入隊列)
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 再次CAS操作嘗試, 成功就設(shè)置互斥鎖的為當(dāng)前線程擁有
setExclusiveOwnerThread(current);
return true;
}
}
// 鎖資源是否被當(dāng)前線程所持有 (可重入鎖)
else if (current == getExclusiveOwnerThread()) {
// 持有鎖資源為當(dāng)前, 則對state + 1
int nextc = c + acquires;
// 健壯性判斷
if (nextc < 0)
// 超過最大鎖重入次數(shù)會拋異常(幾率很小,理論上存在)
throw new Error("Maximum lock count exceeded");
// 設(shè)置state狀態(tài),代表鎖重入成功
setState(nextc);
return true;
}
return false;
}四. AQS的addWaiter方法
addWaiter方法,就是將當(dāng)前線程封裝為Node對象,并且插入到AQS的雙向鏈表。
// 線程入隊列排隊
private Node addWaiter(Node mode) {
// 將當(dāng)前對象封裝為Node對象
// Node.EXCLUSIVE 表示互斥 Node.SHARED 表示共享
Node node = new Node(Thread.currentThread(), mode);
// 獲取tail節(jié)點
Node pred = tail;
// 判斷雙向鏈表隊列有沒有初始化
if (pred != null) {
// 將當(dāng)前線程封裝的Node節(jié)點prev屬性指向tail尾節(jié)點
node.prev = pred;
// 通過CAS操作設(shè)置當(dāng)前線程封裝的Node節(jié)點為尾節(jié)點
if (compareAndSetTail(pred, node)) {
// 成功則將上一個尾節(jié)點的next屬性指向當(dāng)前線程封裝的Node節(jié)點
pred.next = node;
return node;
}
}
// 沒有初始化head 和 tail 都等于null
// enq方法: 插入雙向鏈表和初始化雙向鏈表
enq(node);
// 完成節(jié)點插入
return node;
}
// 插入雙向鏈表和初始化雙向鏈表
private Node enq(final Node node) {
// 死循環(huán)
for (;;) {
// 獲取當(dāng)前tail節(jié)點
Node t = tail;
// 判斷尾節(jié)點是否初始
if (t == null) { // Must initialize
// 通過CAS操作初始化初始化一個虛擬的Node節(jié)點,賦給head節(jié)點
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 完成當(dāng)前線程Node節(jié)點加入AQS雙向鏈表的過程
// 當(dāng)前線程封裝的Node的上一個prev屬性指向tail節(jié)點
// 流程: 1. prev(私有) ---> 2. tail(共有) ---> 3. next (共有)
node.prev = t;
// 通過CAS操作修改tail尾節(jié)點指向當(dāng)前線程封裝的Node
if (compareAndSetTail(t, node)) {
// 將當(dāng)前線程封裝的Node節(jié)點賦給上一個Node的下一個next屬性
t.next = node;
return t;
}
}
}
}五. AQS的acquireQueued方法
acquireQueued方法主要就是線程掛起以及重新嘗試獲取鎖資源的地方
重新獲取鎖資源主要有兩種情況:
- 上來就排在head.next,就回去嘗試拿鎖
- 喚醒之后嘗試拿鎖
// 當(dāng)前線程Node添加到AQS隊列后續(xù)操作
final boolean acquireQueued(final Node node, int arg) {
// 標(biāo)記,記錄拿鎖狀態(tài) 失敗
boolean failed = true;
try {
// 中斷狀態(tài)
boolean interrupted = false;
// 死循環(huán)
for (;;) {
// 獲取當(dāng)前節(jié)點的上一個節(jié)點 prev
final Node p = node.predecessor();
// 判斷當(dāng)前節(jié)點是否是head,是則代表當(dāng)前節(jié)點排在第一位
// 如果是第一位,執(zhí)行tryAcquire方法嘗試拿鎖
if (p == head && tryAcquire(arg)) {
// 都成功,代表拿到鎖資源
// 將當(dāng)前線程Node設(shè)置為head節(jié)點,同時將Node的thread 和 prev屬性設(shè)置為null
setHead(node);
// 將上一個head的next屬性設(shè)置為null,等待GC回收
p.next = null; // help GC
// 拿鎖狀態(tài) 成功
failed = false;
// 返回中斷狀態(tài)
return interrupted;
}
// 沒有獲取到鎖 --- 嘗試掛起線程
// shouldParkAfterFailedAcquire方法: 掛起線程前的準(zhǔn)備
// parkAndCheckInterrupt方法: 掛起當(dāng)前線程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 設(shè)置中斷線程狀態(tài)
interrupted = true;
}
} finally {
// 取消節(jié)點
if (failed)
cancelAcquire(node);
}
}
// 檢查并更新無法獲取鎖節(jié)點的狀態(tài)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 獲取上一個節(jié)點的ws狀態(tài)
/**
* SIGNAL(-1) 表示當(dāng)前節(jié)點釋放鎖的時候,需要喚醒下一個節(jié)點?;蛘哒f后繼節(jié)點在等待當(dāng)前節(jié)點喚醒,后繼節(jié)點入隊時候,會將前驅(qū)節(jié)點更新給signal。
* CANCELLED(1) 表示當(dāng)前節(jié)點已取消調(diào)度。當(dāng)timeout或者中斷情況下,會觸發(fā)變更為此狀態(tài),進(jìn)入該狀態(tài)后的節(jié)點不再變化。
* CONDITION(-2) 當(dāng)其他線程調(diào)用了condition的signal方法后,condition狀態(tài)的節(jié)點會從等待隊列轉(zhuǎn)移到同步隊列中,等待獲取同步鎖。
* PROPAGATE(-3) 表示共享模式下,前驅(qū)節(jié)點不僅會喚醒其后繼節(jié)點,同時也可能喚醒后繼的后繼節(jié)點。
* 默認(rèn)(0) 新節(jié)點入隊時候的默認(rèn)狀態(tài)。
*/
int ws = pred.waitStatus;
// 判斷上個節(jié)點ws狀態(tài)是否是 -1, 是則掛起
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
/**
* 判斷上個節(jié)點是否是取消或者其他狀態(tài)。
* 向前找到不是取消狀態(tài)的節(jié)點,修改ws狀態(tài)。
* 注意:那些放棄的結(jié)點,由于被自己“加塞”到它們前邊,它們相當(dāng)于形成一個無引用鏈,
* 稍后就會被GC回收,這個操作實際是把隊列中的cancelled節(jié)點剔除掉。
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 如果前驅(qū)節(jié)點正常,那就把上一個節(jié)點的狀態(tài)通過CAS的方式設(shè)置成-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
// 掛起當(dāng)前線程
private final boolean parkAndCheckInterrupt() {
// 掛起當(dāng)前線程
LockSupport.park(this);
// 返回中斷標(biāo)志
return Thread.interrupted();
}六. AQS的Lock鎖的release方法
// 互斥鎖模式 解鎖
public final boolean release(int arg) {
// 嘗試是否可以解鎖
if (tryRelease(arg)) {
Node h = head;
// 判斷雙鏈表是否存在線程排隊
if (h != null && h.waitStatus != 0)
// 喚醒后續(xù)線程
unparkSuccessor(h);
return true;
}
return false;
}
// 嘗試是否可以解鎖
protected final boolean tryRelease(int releases) {
// 鎖狀態(tài) = 狀態(tài) - 1
int c = getState() - releases;
// 判斷鎖是是否是當(dāng)前線程持有
if (Thread.currentThread() != getExclusiveOwnerThread())
// 當(dāng)前線程沒有持有拋出異常
throw new IllegalMonitorStateException();
boolean free = false;
// 當(dāng)前鎖狀態(tài)變?yōu)?,則清空鎖歸屬線程
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 設(shè)置鎖狀態(tài)為0
setState(c);
return free;
}
// 喚醒線程
private void unparkSuccessor(Node node) {
// 獲取頭節(jié)點的狀態(tài)
int ws = node.waitStatus;
if (ws < 0)
// 通過CAS將頭節(jié)點的狀態(tài)設(shè)置為初始狀態(tài)
compareAndSetWaitStatus(node, ws, 0);
// 后繼節(jié)點
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 從尾節(jié)點開始往前遍歷,尋找離頭節(jié)點最近的等待狀態(tài)正常的節(jié)點
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 真正的喚醒操作
LockSupport.unpark(s.thread);
}到此這篇關(guān)于淺析Java ReentrantLock鎖的原理與使用的文章就介紹到這了,更多相關(guān)Java ReentrantLock鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
通過spring boot 設(shè)置tomcat解決 post參數(shù)限制問題
這篇文章主要介紹了通過spring boot 設(shè)置tomcat解決 post參數(shù)限制問題,需要的朋友可以參考下2019-05-05
MyBatis-Plus自定義SQL和復(fù)雜查詢的實現(xiàn)
MyBatis-Plus增強了MyBatis的功能,提供注解和XML兩種自定義SQL方式,支持復(fù)雜查詢?nèi)缍啾黻P(guān)聯(lián)、動態(tài)分頁等,通過注解如@Select、@Insert、@Update、@Delete實現(xiàn)CRUD操作,本文就來介紹一下,感興趣的可以了解一下2024-10-10
Spring Boot集成MinIO進(jìn)行文件存儲和管理的詳細(xì)步驟
這篇文章主要介紹了Spring Boot集成MinIO進(jìn)行文件存儲和管理的詳細(xì)步驟,本文通過實例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2025-04-04
Springboot項目使用Slf4j將日志保存到本地目錄的實現(xiàn)代碼
這篇文章主要介紹了Springboot項目使用Slf4j將日志保存到本地目錄的實現(xiàn)方法,本文通過示例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-05-05
一文詳解SpringBoot如何優(yōu)雅地實現(xiàn)異步調(diào)用
SpringBoot想必大家都用過,但是大家平時使用發(fā)布的接口大都是同步的,那么你知道如何優(yōu)雅的實現(xiàn)異步呢?這篇文章就來和大家詳細(xì)聊聊2023-03-03
聊聊springboot靜態(tài)資源加載的規(guī)則
這篇文章主要介紹了springboot靜態(tài)資源加載的規(guī)則,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12
解決 Spring RestTemplate post傳遞參數(shù)時報錯問題
本文詳解說明了RestTemplate post傳遞參數(shù)時報錯的問題及其原由,需要的朋友可以參考下2020-02-02
Mybatis Select Count(*)的返回值類型介紹
這篇文章主要介紹了Mybatis Select Count(*)的返回值類型,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12

