万字图文 | 聊一聊 ReentrantLock 和 AQS 那点事(看完不会你找我)
前言
AbstractQueuedSynchronizer(AQS)是 Java 并發(fā)編程中繞不過(guò)去的一道坎,JUC 并發(fā)包下的 Lock、Semaphore、ReentrantLock 等都是基于 AQS 實(shí)現(xiàn)的。AQS 是一個(gè)抽象的同步框架,提供了原子性管理同步狀態(tài),基于阻塞隊(duì)列模型實(shí)現(xiàn)阻塞和喚醒等待線程的功能
文章從 ReentrantLock 加鎖、解鎖應(yīng)用 API 入手,逐步講解 AQS 對(duì)應(yīng)源碼以及相關(guān)隱含流程
列出本篇文章大綱以及相關(guān)知識(shí)點(diǎn),方便大家更好的理解
什么是 ReentrantLock
ReentrantLock 翻譯為 可重入鎖,指的是一個(gè)線程能夠?qū)?臨界區(qū)共享資源進(jìn)行重復(fù)加鎖
確保線程安全最常見(jiàn)的做法是利用鎖機(jī)制(Lock、sychronized)來(lái)對(duì) 共享數(shù)據(jù)做互斥同步,這樣在同一個(gè)時(shí)刻,只有 一個(gè)線程可以執(zhí)行某個(gè)方法或者某個(gè)代碼塊,那么操作必然是 原子性的,線程安全的
這里就有個(gè)疑問(wèn),因?yàn)?JDK 中關(guān)鍵字 synchronized 也能同時(shí)支持原子性以及線程安全
有了 synchronized 關(guān)鍵字后為什么還需要 ReentrantLock?
為了大家更好的掌握 ReentrantLock 源碼,這里列出兩種鎖之間的區(qū)別
通過(guò)以上六個(gè)維度比對(duì),可以看出 ReentrantLock 是要比 synchronized 靈活以及支持功能更豐富
什么是 AQS
AQS( AbstractQueuedSynchronizer )是一個(gè)用來(lái)構(gòu)建鎖和同步器的抽象框架,只需要繼承 AQS 就可以很方便的實(shí)現(xiàn)我們自定義的多線程同步器、鎖
如圖所示,在 java.util.concurrent 包下相關(guān)鎖、同步器(常用的有 ReentrantLock、 ReadWriteLock、CountDownLatch…)都是基于 AQS 來(lái)實(shí)現(xiàn)
AQS 是典型的模板方法設(shè)計(jì)模式,父類(lèi)(AQS)定義好骨架和內(nèi)部操作細(xì)節(jié),具體規(guī)則由子類(lèi)去實(shí)現(xiàn)
AQS 核心原理
如果被請(qǐng)求的共享資源未被占用,將當(dāng)前請(qǐng)求資源的線程設(shè)置為獨(dú)占線程,并將共享資源設(shè)置為鎖定狀態(tài)
AQS 使用一個(gè) Volatile 修飾的 int 類(lèi)型的成員變量 State 來(lái)表示同步狀態(tài),修改同步狀態(tài)成功即為獲得鎖
Volatile 保證了變量在多線程之間的可見(jiàn)性,修改 State 值時(shí)通過(guò) CAS 機(jī)制來(lái)保證修改的原子性
如果共享資源被占用,需要一定的阻塞等待喚醒機(jī)制來(lái)保證鎖的分配,AQS 中會(huì)將競(jìng)爭(zhēng)共享資源失敗的線程添加到一個(gè)變體的 CLH 隊(duì)列中
關(guān)于支撐 AQS 特性的重要方法及屬性如下:
CLH 隊(duì)列
既然是 AQS 中使用的是 CLH 變體隊(duì)列,我們先來(lái)了解下 CLH 隊(duì)列是什么
CLH:Craig、Landin and Hagersten 隊(duì)列,是 單向鏈表實(shí)現(xiàn)的隊(duì)列。申請(qǐng)線程只在本地變量上自旋,它不斷輪詢前驅(qū)的狀態(tài),如果發(fā)現(xiàn) 前驅(qū)節(jié)點(diǎn)釋放了鎖就結(jié)束自旋
通過(guò)對(duì) CLH 隊(duì)列的說(shuō)明,可以得出以下結(jié)論
AQS 中的隊(duì)列是 CLH 變體的虛擬雙向隊(duì)列,通過(guò)將每條請(qǐng)求共享資源的線程封裝成一個(gè)節(jié)點(diǎn)來(lái)實(shí)現(xiàn)鎖的分配
相比于 CLH 隊(duì)列而言,AQS 中的 CLH 變體等待隊(duì)列擁有以下特性
認(rèn)識(shí) AOS
抽象類(lèi) AQS 同樣繼承自抽象類(lèi) AOS(AbstractOwnableSynchronizer)
AOS 內(nèi)部只有一個(gè) Thread 類(lèi)型的變量,提供了獲取和設(shè)置當(dāng)前獨(dú)占鎖線程的方法
主要作用是 記錄當(dāng)前占用獨(dú)占鎖(互斥鎖)的線程實(shí)例
public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {// 獨(dú)占線程(不參與序列化)private transient Thread exclusiveOwnerThread;// 設(shè)置當(dāng)前獨(dú)占的線程protected final void setExclusiveOwnerThread(Thread thread) {exclusiveOwnerThread = thread;}// 返回當(dāng)前獨(dú)占的線程protected final Thread getExclusiveOwnerThread() {return exclusiveOwnerThread;} }為什么要掌握 AQS
如何能夠體現(xiàn)程序員的水平,那就是掌握大多數(shù)人所不掌握的技術(shù),這也是為什么面試時(shí) AQS 高頻出現(xiàn)的原因,因?yàn)樗缓?jiǎn)單
最初接觸 ReentrantLock 以及 AQS 的時(shí)候,看到源碼就是一頭霧水,Debug 跟著跟著就 迷失了自己,相信這也是大多數(shù)人的反應(yīng)
正是因?yàn)榻?jīng)歷過(guò),所以才能從小白的心理上出發(fā),把其中的知識(shí)點(diǎn)能夠盡數(shù)梳理
作者寫(xiě)的很用心,看過(guò)這篇文章的小伙伴,不敢保證百分百理解 AQS 和 ReentrantLock 的原理,但是一定會(huì)有所收獲
獨(dú)占加鎖源碼解析
什么是獨(dú)占鎖
獨(dú)占鎖也叫排它鎖,是指該鎖一次只能被一個(gè)線程所持有,如果別的線程想要獲取鎖,只有等到持有鎖線程釋放
獲得排它鎖的線程即能讀數(shù)據(jù)又能修改數(shù)據(jù),與之對(duì)立的就是共享鎖
共享鎖是指該鎖可被多個(gè)線程所持有。如果線程T對(duì)數(shù)據(jù)A加上共享鎖后,則其他線程只能對(duì)A再加共享鎖,不能加排它鎖
獲得共享鎖的線程只能讀數(shù)據(jù),不能修改數(shù)據(jù)
獨(dú)占鎖加鎖
ReentrantLock 就是獨(dú)占鎖的一種實(shí)現(xiàn)方式,接下來(lái)看代碼中如何使用 ReentrantLock 完成獨(dú)占式加鎖業(yè)務(wù)邏輯
public static void main(String[] args) {// 創(chuàng)建非公平鎖ReentrantLock lock = new ReentrantLock();// 獲取鎖操作lock.lock();try {// 執(zhí)行代碼邏輯} catch (Exception ex) {// ...} finally {// 解鎖操作lock.unlock();} }new ReentrantLock() 構(gòu)造函數(shù)默認(rèn)創(chuàng)建的是非公平鎖 NonfairSync
public ReentrantLock() {sync = new NonfairSync(); }同時(shí)也可以在創(chuàng)建鎖構(gòu)造函數(shù)中傳入具體參數(shù)創(chuàng)建公平鎖 FairSync
ReentrantLock lock = new ReentrantLock(true); --- ReentrantLock // true 代表公平鎖,false 代表非公平鎖 public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync(); }FairSync、NonfairSync 代表公平鎖和非公平鎖,兩者都是 ReentrantLock 靜態(tài)內(nèi)部類(lèi),只不過(guò)實(shí)現(xiàn)不同鎖語(yǔ)義
公平鎖 FairSync
非公平鎖 NonfairSync
兩者的都繼承自 ReentrantLock 靜態(tài)抽象內(nèi)部類(lèi) Sync,Sync 類(lèi)繼承自 AQS,這里就有個(gè)疑問(wèn)
這些鎖都沒(méi)有直接繼承 AQS,而是定義了一個(gè) Sync 類(lèi)去繼承 AQS,為什么要這樣呢?
因?yàn)?鎖面向的是使用用戶,同步器面向的則是線程控制,那么在鎖的實(shí)現(xiàn)中聚合同步器而不是直接繼承 AQS 就可以很好的 隔離二者所關(guān)注的事情
通過(guò)對(duì)不同鎖種類(lèi)的講解以及 ReentrantLock 內(nèi)部結(jié)構(gòu)的解析,根據(jù)上下級(jí)關(guān)系繼承圖,加深其理解
這里以非公平鎖舉例,查看加鎖的具體過(guò)程,詳細(xì)信息下文會(huì)詳細(xì)說(shuō)明
看一下非公平鎖加鎖方法 lock 內(nèi)部怎么做的
ReentrantLock lock = new ReentrantLock(); lock.lock(); --- ReentrantLock public void lock() {sync.lock(); } --- Sync abstract void lock();Sync#lock 為抽象方法,最終會(huì)調(diào)用其子類(lèi)非公平鎖的方法 lock
final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1); }非公平加鎖方法有兩個(gè)邏輯
compareAndSetState 以 CAS 比較并替換的方式將 State 值設(shè)置為 1,表示同步狀態(tài)被占用
protected final boolean compareAndSetState(int expect, int update) {// See below for intrinsics setup to support thisreturn unsafe.compareAndSwapInt(this, StateOffset, expect, update); }setExclusiveOwnerThread 設(shè)置當(dāng)前線程為獨(dú)占鎖擁有線程
protected final void setExclusiveOwnerThread(Thread thread) {exclusiveOwnerThread = thread; }acquire 對(duì)整個(gè) AQS 做到了承上啟下的作用,通過(guò) tryAcquire 模版方法進(jìn)行嘗試獲取鎖,獲取鎖失敗包裝當(dāng)前線程為 Node 節(jié)點(diǎn)加入等待隊(duì)列排隊(duì)
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt(); }tryAcquire 是 AQS 中抽象模版方法,但是內(nèi)部會(huì)有默認(rèn)實(shí)現(xiàn),雖然默認(rèn)的方法內(nèi)部拋出異常,為什么不直接定義為抽象方法呢?
因?yàn)?AQS 不只是對(duì)獨(dú)占鎖實(shí)現(xiàn)了抽象,同時(shí)還包括共享鎖;不同鎖定義了不同類(lèi)別的方法,共享鎖就不需要 tryAcquire,如果定義為抽象方法,繼承 AQS 子類(lèi)都需要實(shí)現(xiàn)該方法
protected boolean tryAcquire(int arg) {throw new UnsupportedOperationException(); }NonfairSync 類(lèi)中有 tryAcquire 重寫(xiě)方法,繼續(xù)查看具體如何進(jìn)行非公平方式獲取鎖
protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires); }final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();// State 等于0表示此時(shí)無(wú)鎖if (c == 0) {// 再次使用CAS嘗試獲取鎖, 表現(xiàn)為非公平鎖特性if (compareAndSetState(0, acquires)) {// 設(shè)置線程為獨(dú)占鎖線程setExclusiveOwnerThread(current);return true;}// 如果當(dāng)前線程等于已獲取鎖線程, 表現(xiàn)為可重入鎖特性} else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");// 設(shè)置 StatesetState(nextc);return true;}// 如果state不等于0并且獨(dú)占線程不是當(dāng)前線程, 返回 falsereturn false; }由于 tryAcquire 做了取反,如果設(shè)置 state 失敗并且獨(dú)占鎖線程不是自己本身返回 false,通過(guò)取反會(huì)進(jìn)入接下來(lái)的流程
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt(); }Node 入隊(duì)流程
嘗試獲得鎖失敗,接下來(lái)會(huì)將線程組裝成為 Node 進(jìn)行入隊(duì)流程
Node 是 AQS 中最基本的數(shù)據(jù)結(jié)構(gòu),也是 CLH 變體隊(duì)列中的節(jié)點(diǎn),Node 有 SHARED(共享)、EXCLUSIVE(獨(dú)占) 兩種模式,文章主要介紹 EXCLUSIVE 模式,不相關(guān)的屬性和方法不予介紹
下面列出關(guān)于 Node EXCLUSIVE 模式的一些關(guān)鍵方法以及狀態(tài)信息
| waitStatus | 當(dāng)前節(jié)點(diǎn)在隊(duì)列中處于什么狀態(tài) |
| thread | 表示節(jié)點(diǎn)對(duì)應(yīng)的線程 |
| prev | 前驅(qū)指針,指向本節(jié)點(diǎn)的上一個(gè)節(jié)點(diǎn) |
| next | 后繼指針,指向本節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn) |
| predecessor | 返回前驅(qū)節(jié)點(diǎn),沒(méi)有的話拋出 NPE 異常 |
Node 中獨(dú)占鎖相關(guān)的 waitStatus 屬性分別有以下幾種狀態(tài)
| 0 | Node 被初始化后的默認(rèn)值 |
| CANCELLED | 值為1,由于中斷或超時(shí),節(jié)點(diǎn)被取消 |
| SIGNAL | 值為-1,表示節(jié)點(diǎn)的后繼節(jié)點(diǎn)即將被阻塞 |
| CONDITION | 值為-2,表示節(jié)點(diǎn)在等待隊(duì)列中,節(jié)點(diǎn)線程等待喚醒 |
介紹完 Node 相關(guān)基礎(chǔ)知識(shí),看一下請(qǐng)求鎖線程如何被包裝為 Node,又是如何初始化入隊(duì)的
private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode);// 獲取等待隊(duì)列的尾節(jié)點(diǎn)Node pred = tail;// 如果尾節(jié)點(diǎn)不為空, 將 node 設(shè)置為尾節(jié)點(diǎn), 并將原尾節(jié)點(diǎn) next 指向 新的尾節(jié)點(diǎn)nodeif (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}// 尾部為空,enq 執(zhí)行enq(node);return node; }pred 為隊(duì)列的尾節(jié)點(diǎn),根據(jù)尾節(jié)點(diǎn)是否為空會(huì)執(zhí)行對(duì)應(yīng)流程
enq 方法執(zhí)行初始化隊(duì)列操作,等待隊(duì)列中虛擬化的頭節(jié)點(diǎn)也是在這里產(chǎn)生
private Node enq(final Node node) {for (; ; ) {Node t = tail;if (t == null) {// 虛擬化一個(gè)空Node, 并將head指向空Nodeif (compareAndSetHead(new Node()))// 將尾節(jié)點(diǎn)等于頭節(jié)點(diǎn)tail = head;} else {// node上一條指向尾節(jié)點(diǎn)node.prev = t;// 設(shè)置node為尾節(jié)點(diǎn)if (compareAndSetTail(t, node)) {// 設(shè)置原尾節(jié)點(diǎn)的下一條指向nodet.next = node;return t;}}} }執(zhí)行 enq 方法的前提就是隊(duì)列尾節(jié)點(diǎn)為空,為什么還要再判斷尾節(jié)點(diǎn)是否為空?
因?yàn)?enq 方法中是一個(gè)死循環(huán),循環(huán)過(guò)程中 t 的值是不固定的。假如執(zhí)行 enq 方法時(shí)隊(duì)列為空,for 循環(huán)會(huì)執(zhí)行兩遍不同的處理邏輯
畫(huà)兩張圖來(lái)理解 enq 方法整體初始化 AQS 隊(duì)列流程,假設(shè)T1、T2兩個(gè)線程爭(zhēng)取鎖,T1成功獲得鎖,T2進(jìn)行入隊(duì)操作
addWaiter 方法就是為了讓 Node 入隊(duì),并且維護(hù)出一個(gè)雙向隊(duì)列模型
入隊(duì)執(zhí)行成功后,會(huì)在 acquireQueued 再次嘗試競(jìng)爭(zhēng)鎖,競(jìng)爭(zhēng)失敗后會(huì)將線程阻塞
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt(); }acquireQueued 方法會(huì)嘗試自旋獲取鎖,獲取失敗對(duì)當(dāng)前線程實(shí)施阻塞流程,這也是為了避免無(wú)意義的自旋,對(duì)比 CLH 隊(duì)列性能優(yōu)化的體現(xiàn)
final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (; ; ) {// 獲取node上一個(gè)節(jié)點(diǎn)final Node p = node.predecessor();// 如果node為頭節(jié)點(diǎn) & 嘗試獲取鎖成功if (p == head && tryAcquire(arg)) {// 此時(shí)當(dāng)前node線程獲取到了鎖// 將node設(shè)置為新的頭節(jié)點(diǎn)setHead(node);// help GCp.next = null;failed = false;return interrupted;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);} }通過(guò) node.predecessor() 獲取節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn),前驅(qū)節(jié)點(diǎn)為空拋出空指針異常
final Node predecessor() throws NullPointerException {Node p = prev;if (p == null)throw new NullPointerException();elsereturn p; }獲取到前驅(qū)節(jié)點(diǎn)后進(jìn)行兩步邏輯判斷
如果 node 獲得鎖后,setHead 將節(jié)點(diǎn)設(shè)置為隊(duì)列頭,從而實(shí)現(xiàn)出隊(duì)效果,出于 GC 的考慮,清空未使用的數(shù)據(jù)
private void setHead(Node node) {head = node;node.thread = null;node.prev = null; }shouldParkAfterFailedAcquire 需要重點(diǎn)關(guān)注下,流程相對(duì)比較難理解
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;if (ws == Node.SIGNAL)return true;if (ws > 0) {do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false; }ws 表示為當(dāng)前申請(qǐng)鎖節(jié)點(diǎn)前驅(qū)節(jié)點(diǎn)的等待狀態(tài),代碼中包含三個(gè)邏輯,分別是:
設(shè)置當(dāng)前節(jié)點(diǎn)的前置節(jié)點(diǎn)等待狀態(tài)為 Node.SIGNAL,表示當(dāng)前節(jié)點(diǎn)獲取鎖失敗,需要進(jìn)行阻塞操作
還是通過(guò)幾張圖來(lái)理解流程,假設(shè)此時(shí) T1、T2 線程來(lái)爭(zhēng)奪鎖
T1 線程獲得鎖,T2 進(jìn)入 AQS 等待隊(duì)列排隊(duì),并通過(guò) CAS 將 T2 節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)等待狀態(tài)置為 SIGNAL
執(zhí)行切換前驅(qū)節(jié)點(diǎn)等待狀態(tài)后返回 false,繼續(xù)進(jìn)行循環(huán)嘗試獲取同步狀態(tài)
這一步操作保證了線程能進(jìn)行多次重試,盡量避免線程狀態(tài)切換
如果 T1 線程沒(méi)有釋放鎖,T2 線程第二次執(zhí)行到 shouldParkAfterFailedAcquire 方法,因?yàn)榍膀?qū)節(jié)點(diǎn)已設(shè)置為 SIGNAL,所以會(huì)直接返回 true,執(zhí)行線程阻塞操作
private final boolean parkAndCheckInterrupt() {// 將當(dāng)前線程進(jìn)行阻塞LockSupport.park(this);// 方法返回了當(dāng)前線程的中斷狀態(tài),并將當(dāng)前線程的中斷標(biāo)識(shí)設(shè)置為falsereturn Thread.interrupted(); }LockSupport.park 方法將當(dāng)前等待隊(duì)列中線程進(jìn)行阻塞操作,線程執(zhí)行一個(gè)從 RUNNABLE 到 WAITING 狀態(tài)轉(zhuǎn)變
如果線程被喚醒,通過(guò)執(zhí)行 Thread.interrupted 查看中斷狀態(tài),這里的中斷狀態(tài)會(huì)被傳遞到 acquire 方法
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 如果線程被中斷, 這里會(huì)再次設(shè)置中斷狀態(tài)// 因?yàn)槿绻€程中斷, 調(diào)用 Thread.interrupted 雖然會(huì)返回 true, 但是會(huì)清除線程中斷狀態(tài)selfInterrupt(); }即使線程從 park 方法中喚醒后發(fā)現(xiàn)自己被中斷了,但是不影響接下來(lái)的獲取鎖操作,如果需要設(shè)置線程中斷來(lái)影響流程,可以使用 lockInterruptibly 獲得鎖,拋出檢查異常 InterruptedException
cancelAcquire
取消排隊(duì)方法是 AQS 中比較難的知識(shí)點(diǎn),不容易被理解
當(dāng)線程因?yàn)樽孕蛘弋惓5惹闆r獲取鎖失敗,會(huì)調(diào)用此方法進(jìn)行取消正在獲取鎖的操作
private void cancelAcquire(Node node) {// 不存在的節(jié)點(diǎn)直接返回if (node == null)return; node<span class="token punctuation">.</span>thread <span class="token operator">=</span> null<span class="token punctuation">;</span><span class="token comment">/*** waitStatus > 0 代表節(jié)點(diǎn)為取消狀態(tài)* while循環(huán)會(huì)將node節(jié)點(diǎn)的前驅(qū)指針指向一個(gè)非取消狀態(tài)的節(jié)點(diǎn)* pred等于當(dāng)前節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)(非取消狀態(tài))*/</span> Node pred <span class="token operator">=</span> node<span class="token punctuation">.</span>prev<span class="token punctuation">;</span> <span class="token keyword">while</span> <span class="token punctuation">(</span>pred<span class="token punctuation">.</span>waitStatus <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span>node<span class="token punctuation">.</span>prev <span class="token operator">=</span> pred <span class="token operator">=</span> pred<span class="token punctuation">.</span>prev<span class="token punctuation">;</span><span class="token comment">// 獲取過(guò)濾后的前驅(qū)節(jié)點(diǎn)的后繼節(jié)點(diǎn)</span> Node predNext <span class="token operator">=</span> pred<span class="token punctuation">.</span>next<span class="token punctuation">;</span><span class="token comment">// 設(shè)置node等待狀態(tài)為取消狀態(tài)</span> node<span class="token punctuation">.</span>waitStatus <span class="token operator">=</span> Node<span class="token punctuation">.</span>CANCELLED<span class="token punctuation">;</span><span class="token comment">// 🚩步驟一,如果node是尾節(jié)點(diǎn),使用CAS將pred設(shè)置為新的尾節(jié)點(diǎn)</span><span class="token keyword">if</span> <span class="token punctuation">(</span>node <span class="token operator">==</span> tail <span class="token operator">&&</span> <span class="token function">compareAndSetTail</span><span class="token punctuation">(</span>node<span class="token punctuation">,</span> pred<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span><span class="token comment">// 設(shè)置pred(新tail)的后驅(qū)指針為空</span><span class="token function">compareAndSetNext</span><span class="token punctuation">(</span>pred<span class="token punctuation">,</span> predNext<span class="token punctuation">,</span> null<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{<!-- --></span><span class="token keyword">int</span> ws<span class="token punctuation">;</span><span class="token comment">// 🚩步驟二,node的前驅(qū)節(jié)點(diǎn)pred(非取消狀態(tài))!= 頭節(jié)點(diǎn)</span><span class="token keyword">if</span> <span class="token punctuation">(</span>pred <span class="token operator">!=</span> head <span class="token comment">/*** 1. pred等待狀態(tài)等于SIGNAL* 2. ws <= 0并且設(shè)置pred等待狀態(tài)為SIGNAL*/</span><span class="token operator">&&</span> <span class="token punctuation">(</span><span class="token punctuation">(</span>ws <span class="token operator">=</span> pred<span class="token punctuation">.</span>waitStatus<span class="token punctuation">)</span> <span class="token operator">==</span> Node<span class="token punctuation">.</span>SIGNAL <span class="token operator">||</span> <span class="token punctuation">(</span>ws <span class="token operator"><=</span> <span class="token number">0</span> <span class="token operator">&&</span> <span class="token function">compareAndSetWaitStatus</span><span class="token punctuation">(</span>pred<span class="token punctuation">,</span> ws<span class="token punctuation">,</span> Node<span class="token punctuation">.</span>SIGNAL<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token comment">// pred中線程不為空</span><span class="token operator">&&</span> pred<span class="token punctuation">.</span>thread <span class="token operator">!=</span> null<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>Node next <span class="token operator">=</span> node<span class="token punctuation">.</span>next<span class="token punctuation">;</span><span class="token comment">/*** 1. 當(dāng)前節(jié)點(diǎn)的后繼節(jié)點(diǎn)不為空* 2. 后繼節(jié)點(diǎn)等待狀態(tài)<=0(表示非取消狀態(tài))*/</span><span class="token keyword">if</span> <span class="token punctuation">(</span>next <span class="token operator">!=</span> null <span class="token operator">&&</span> next<span class="token punctuation">.</span>waitStatus <span class="token operator"><=</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token comment">// 設(shè)置pred的后繼節(jié)點(diǎn)設(shè)置為當(dāng)前節(jié)點(diǎn)的后繼節(jié)點(diǎn)</span><span class="token function">compareAndSetNext</span><span class="token punctuation">(</span>pred<span class="token punctuation">,</span> predNext<span class="token punctuation">,</span> next<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{<!-- --></span><span class="token comment">// 🚩步驟三,如果當(dāng)前節(jié)點(diǎn)為頭節(jié)點(diǎn)或者上述條件不滿足, 執(zhí)行喚醒當(dāng)前節(jié)點(diǎn)的后繼節(jié)點(diǎn)流程</span><span class="token function">unparkSuccessor</span><span class="token punctuation">(</span>node<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span>node<span class="token punctuation">.</span>next <span class="token operator">=</span> node<span class="token punctuation">;</span> <span class="token comment">// help GC</span> <span class="token punctuation">}</span>}
邏輯稍微復(fù)雜一些,比較重要是以下三個(gè)邏輯
步驟一當(dāng)前節(jié)點(diǎn)為尾節(jié)點(diǎn)的話,設(shè)置 pred 節(jié)點(diǎn)為新的尾節(jié)點(diǎn),成功設(shè)置后再將 pred 后繼節(jié)點(diǎn)設(shè)置為空(尾節(jié)點(diǎn)不會(huì)有后繼節(jié)點(diǎn))
步驟二需要滿足以下四個(gè)條件才會(huì)將前驅(qū)節(jié)點(diǎn)(非取消狀態(tài))的后繼指針指向當(dāng)前節(jié)點(diǎn)的后繼指針
1)當(dāng)前節(jié)點(diǎn)不等于尾節(jié)點(diǎn)
2)當(dāng)前節(jié)點(diǎn)前驅(qū)節(jié)點(diǎn)不等于頭節(jié)點(diǎn)
3)前驅(qū)節(jié)點(diǎn)的等待狀態(tài)不為取消狀態(tài)
4)前驅(qū)節(jié)點(diǎn)的擁有線程不為空
如果不滿足步驟二的話,會(huì)執(zhí)行步驟三相關(guān)邏輯,喚醒后繼節(jié)點(diǎn)
步驟一:
假設(shè)當(dāng)前取消節(jié)點(diǎn)為尾節(jié)點(diǎn)并且前置節(jié)點(diǎn)無(wú)取消節(jié)點(diǎn),現(xiàn)有等待隊(duì)列如下圖,執(zhí)行下述邏輯
if (node == tail && compareAndSetTail(node, pred)) {compareAndSetNext(pred, predNext, null); }將 pred 設(shè)置為新的尾節(jié)點(diǎn),并將 pred 后繼節(jié)點(diǎn)設(shè)置為空,因?yàn)槲补?jié)點(diǎn)不會(huì)有后繼節(jié)點(diǎn)了
T4 線程所在節(jié)點(diǎn)因無(wú)引用指向,會(huì)被 GC 垃圾回收處理
步驟二:
如果當(dāng)前需要取消節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)為取消狀態(tài)節(jié)點(diǎn),如圖所示
設(shè)置 pred(非取消狀態(tài))的后繼節(jié)點(diǎn)為 node 的后繼節(jié)點(diǎn),并設(shè)置 node 的 next 為 自己本身
線程T2、T3所在節(jié)點(diǎn)因?yàn)楸籘4所直接或間接指向,如何進(jìn)行GC?
AQS 等待隊(duì)列中取消狀態(tài)節(jié)點(diǎn)會(huì)在 shouldParkAfterFailedAcquire 方法中被 GC 垃圾回收
if (ws > 0) {do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node; }T4 線程所在節(jié)點(diǎn)獲取鎖失敗嘗試停止時(shí),會(huì)執(zhí)行上述代碼,執(zhí)行后的等待隊(duì)列如下圖所示
等待隊(duì)列中取消狀態(tài)節(jié)點(diǎn)就可以被 GC 垃圾回收了,至此加鎖流程也就結(jié)束了,下面繼續(xù)看如何解鎖
獨(dú)占解鎖源碼解析
解鎖流程相對(duì)于加鎖簡(jiǎn)單了很多,調(diào)用對(duì)應(yīng)API-lock.unlock()
--- ReentrantLock public void unlock() {sync.release(1); } --- AQS public final boolean release(int arg) {// 嘗試釋放鎖if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false; }釋放鎖同步狀態(tài)
tryRelease 是定義在 AQS 中的抽象方法,通過(guò) Sync 類(lèi)重寫(xiě)了其實(shí)現(xiàn)
protected final boolean tryRelease(int releases) {int c = getState() - releases;// 如果當(dāng)前線程不等于擁有鎖線程, 拋出異常if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true;// 將擁有鎖線程設(shè)置為空setExclusiveOwnerThread(null);}// 設(shè)置State狀態(tài)為0, 解鎖成功setState(c);return free; }喚醒后繼節(jié)點(diǎn)
此時(shí) State 值已被釋放,對(duì)于頭節(jié)點(diǎn)的判斷這塊流程比較有意思
Node h = head; if (h != null && h.waitStatus != 0)unparkSuccessor(h);什么情況下頭節(jié)點(diǎn)為空,當(dāng)線程還在爭(zhēng)奪鎖,隊(duì)列還未初始化,頭節(jié)點(diǎn)必然是為空的
當(dāng)頭節(jié)點(diǎn)等待狀態(tài)等于0,證明后繼節(jié)點(diǎn)還在自旋,不需要進(jìn)行后繼節(jié)點(diǎn)喚醒
如果同時(shí)滿足上述兩個(gè)條件,會(huì)對(duì)等待隊(duì)列頭節(jié)點(diǎn)的后繼節(jié)點(diǎn)進(jìn)行喚醒操作
private void unparkSuccessor(Node node) {// 獲取node等待狀態(tài)int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);// 獲取node的后繼節(jié)點(diǎn)Node s = node.next;// 如果下個(gè)節(jié)點(diǎn)為空或者被取消, 遍歷隊(duì)列查詢非取消節(jié)點(diǎn)if (s == null || s.waitStatus > 0) {s = null;// 從隊(duì)尾開(kāi)始查找, 等待狀態(tài) <= 0 的節(jié)點(diǎn)for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}// 滿足 s != null && s.waitStatus <= 0// 執(zhí)行 unparkif (s != null)LockSupport.unpark(s.thread); }為什么查找隊(duì)列中未被取消的節(jié)點(diǎn)需要從尾部開(kāi)始?
這個(gè)問(wèn)題有兩個(gè)原因可以解釋,分別是 Node 入隊(duì)和清理取消狀態(tài)的節(jié)點(diǎn)
先從 addWaiter 入隊(duì)時(shí)說(shuō)起,compareAndSetTail(pred, node)、pred.next = node 并非原子操作,如果在執(zhí)行 pred.next = node 前進(jìn)行 unparkSuccessor,就沒(méi)有辦法通過(guò) next 指針向后遍歷,所以才會(huì)從后向前找尋非取消的節(jié)點(diǎn)
cancelAcquire 方法也有導(dǎo)致使用 head 無(wú)法遍歷全部 Node 的因素,因?yàn)橄葦嚅_(kāi)的是 next 指針,prev 指針并未斷開(kāi)
喚醒阻塞后流程
當(dāng)線程獲取鎖失敗被 park 后進(jìn)入了阻塞模式,前驅(qū)節(jié)點(diǎn)釋放鎖后會(huì)進(jìn)行喚醒 unpark,被阻塞線程狀態(tài)回歸 RUNNABLE 狀態(tài)
private final boolean parkAndCheckInterrupt() {// 從此位置喚醒LockSupport.park(this);return Thread.interrupted(); }被喚醒線程檢查自身是否被中斷,返回自身中斷狀態(tài)到 acquireQueued
final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (; ; ) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return interrupted;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);} }假設(shè)自身被中斷,設(shè)置 interrupted = true,繼續(xù)通過(guò)循環(huán)嘗試獲取鎖,獲取鎖成功后返回 interrupted 中斷狀態(tài)
中斷狀態(tài)本身并不會(huì)對(duì)加鎖流程產(chǎn)生影響,被喚醒后還是會(huì)不斷進(jìn)行獲取鎖,直到獲取鎖成功進(jìn)行返回,返回中斷狀態(tài)是為了后續(xù)補(bǔ)充中斷紀(jì)錄
如果線程被喚醒后發(fā)現(xiàn)中斷,成功獲取鎖后會(huì)將中斷狀態(tài)返回,補(bǔ)充中斷狀態(tài)
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt(); }selfInterrupt 就是對(duì)線程中斷狀態(tài)的一個(gè)補(bǔ)充,補(bǔ)充狀態(tài)成功后,流程結(jié)束
static void selfInterrupt() {Thread.currentThread().interrupt(); }閱讀源碼小技巧
1、從全局掌握要閱讀的源碼提供了什么功能
這也是我一直推崇的學(xué)習(xí)源碼方式,學(xué)習(xí)源碼的關(guān)鍵點(diǎn)是抓住主線流程,在了解主線之前不要最開(kāi)始就研究到源碼實(shí)現(xiàn)細(xì)節(jié)中,否則很容易迷失在細(xì)枝末節(jié)的代碼中
以文章中的 AQS 舉例,當(dāng)你知道了它是一個(gè)抽象隊(duì)列同步器,使用它可以更簡(jiǎn)單的構(gòu)造鎖和同步器等實(shí)現(xiàn)
然后從中理解 tryAcquire、tryRelease 等方法實(shí)現(xiàn),這樣是不是可以更好的理解與 AQS 與其子類(lèi)相關(guān)的代碼
2、把不易理解的源碼粘貼出來(lái),整理好格式打好備注
一般源碼中的行為格式和我們?nèi)粘G么a是不一樣的,而且 JDK 源碼中的變量命名實(shí)在是慘不忍睹
所以就應(yīng)該將難以理解的源碼粘貼出,標(biāo)上對(duì)應(yīng)注釋以及調(diào)整成易理解的格式,這樣對(duì)于源碼的閱讀就會(huì)輕松很多
后記
平常工作中接觸到 AQS 相關(guān)知識(shí)還是很多的,知其然知其所以然,文章以 ReentrantLock 作為切入點(diǎn),講述了其公平鎖和非公平鎖的概念,以及對(duì)應(yīng) AQS 中 CLH、AOS 等不容易被發(fā)現(xiàn)的概念
針對(duì) ReentrantLock 以及 AQS 加鎖、解鎖、排隊(duì)等流程進(jìn)行了詳細(xì)說(shuō)明,以圖文并茂的方式講述了其流程源碼實(shí)現(xiàn)細(xì)節(jié),這里希望在看的小伙伴都能收獲 AQS 相關(guān)知識(shí)
由于作者水平有限, 歡迎大家能夠反饋指正文章中錯(cuò)誤不正確的地方, 感謝 🙏
小伙伴的喜歡就是對(duì)我最大的支持, 如果讀了文章有所收獲, 希望能夠 點(diǎn)贊、評(píng)論、關(guān)注三連!
參考資料
- https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html
- https://tech.meituan.com/2018/11/15/java-lock.html
- https://mp.weixin.qq.com/s/y_e3ciU-hiqlb5vseuOFyw
- https://zhuanlan.zhihu.com/p/197840259?utm_source=wechat_session
- https://blog.csdn.net/java_lyvee/article/details/98966684
- https://zhuanlan.zhihu.com/p/197840259?utm_source=wechat_session
總結(jié)
以上是生活随笔為你收集整理的万字图文 | 聊一聊 ReentrantLock 和 AQS 那点事(看完不会你找我)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 乐观锁和悲观锁的使用场景及应用——Jav
- 下一篇: HttpServletResponse和