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