摘要:本章我們主要聊獨占式即同一時刻只能有一個線程獲取同步狀態,其它獲取同步狀態失敗的線程則會加入到同步隊列中進行等待。到這獨占式獲取同步和釋放同步狀態的源碼已經分析完了。
一、寫在前面
上篇文章通過ReentrantLock 的加鎖和釋放鎖過程給大家聊了聊AQS架構以及實現原理,具體參見《J.U.C|AQS的原理》。
理解了原理,我們在來看看再來一步一步的聊聊其源碼是如何實現的。
本章給大家聊聊AQS中獨占式獲取和釋放共享狀態的流程,主要根據tryAcquire(int arg) -- > tryRelease(int arg)來講。
二、什么是獨占式AQS的同步隊列提供兩種模式即獨占式(EXCLUSIVE) 和 共享式(SHARED)。
本章我們主要聊獨占式: 即同一時刻只能有一個線程獲取同步狀態,其它獲取同步狀態失敗的線程則會加入到同步隊列中進行等待。
主要講解方法:
tryAcquire(int):獨占方式。嘗試獲取資源,成功則返回true,失敗則返回false。
tryRelease(int):獨占方式。嘗試釋放資源,成功則返回true,失敗則返回false。
有對同步隊列不明白的請看《J.U.C|同步隊列(CLH)》
三、核心方法分析acquire(int arg)
獨占式獲取同步狀態的頂級入口acquire(int arg)方法,如果線程獲取到共享狀態則直接返回, 否則把當前線程構造成獨占式(node.EXCLUSIVE)模式節點并添加到同步隊列尾部,直到獲取到共享狀態為止,整個過程忽略中斷。
方法源碼
public final void acquire(int arg) { ? ? ? ? if (!tryAcquire(arg) && ? ? ? ? ? ? acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) ? ? ? ? ? ? selfInterrupt(); ? ? } }
方法函數:
tryAcquire(arg):嘗試獲取同步狀態、獲取成功則直接返回。
addWaiter(Node.EXCLUSIVE):當同步狀態獲取失敗時,構建一個獨占式節點并將其加入到同步隊列的尾部。
acquireQueued(Node, arg)) : 獲取該節點指定數量的資源,通過自旋的方式直到獲取成功,返回是該節點線程的中斷狀態。
selfInterrupt(): 將中斷補上(因其獲取資源的整個過程是忽略中斷的所以最后手動將中斷補上)
源碼分析
tryAcquire(arg)
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
??? 什么鬼? 直接拋出異常? AQS 中對共享狀態的獲取沒有提供具體的實現,等待子類根據自己的場景去實現。有沒有人疑惑,那為什么不是 abstract 的尼? 因為AQS不止是獨占模式的鎖需要繼承它還有別人也需要繼承它,總不能讓別人也來實現一個無關的方法吧。
addWaiter(Node node)
private Node addWaiter(Node mode) { // 以給定的模式來構建節點, mode有兩種模式 // 共享式SHARED, 獨占式EXCLUSIVE; Node node = new Node(Thread.currentThread(), mode); // 嘗試快速將該節點加入到隊列的尾部 Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 如果快速加入失敗,則通過 anq方式入列 enq(node); return node; }
addWaiter(Node mode) 方法嘗試將當前Node節點快速加入到隊列的尾部,如果快速加入失敗則通過enq(node)方法自旋加入。
enq(final Node node)
private Node enq(final Node node) { // CAS自旋,直到加入隊尾成功 for (;;) { Node t = tail; if (t == null) { // 如果隊列為空,則必須先初始化CLH隊列,新建一個空節點標識作為Hader節點,并將tail 指向它 if (compareAndSetHead(new Node())) tail = head; } else {// 正常流程,加入隊列尾部 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
enq(final Node node) 方法通過自旋的方式將當前Node節點加入到隊列尾部,直到成功為止。
注: 在這不管是快速還是自旋的方式將當前Node節點加入到隊列尾部都是通過compareAndSetTail(t, node) 來保證線程安全的,這也是典型實現無鎖化線程安全的方式,CAS自旋volatile變量。
acquireQueued(final Node, int arg)
final boolean acquireQueued(final Node node, int arg) { // 是否拿到資源 boolean failed = true; try { // 標記等待過程中是否被中斷過 boolean interrupted = false; // 自旋 for (;;) { // 獲取當前節點的前驅節點 final Node p = node.predecessor(); // 如果其前驅節點為head 節點,說明此節點有資格去獲取資源了。(可能是被前驅節點喚醒,也可能被interrupted了的) if (p == head && tryAcquire(arg)) { // 拿到資源后將自己設置為head節點, setHead(node); // 將前驅節點 p.next = nul 在setHead(node); 中已經將node.prev = null 設置為空了,方便GC回收前驅節點,也相當于出列。 p.next = null; // help GC failed = false; return interrupted; } // 如果不符合上述條件,說明自己可以休息了,進入waiting狀態,直到被unpark() if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } finally { if (failed) cancelAcquire(node); } }
當前節點的線程在‘死循環’中嘗試獲取同步狀態,前提是只有其前驅節點為head節點時才有嘗試獲取同步狀態的資格,否則繼續在同步隊列中等待被喚醒。
Why?
因為只有head是成功獲取同步狀態的節點,而head節點的線程在釋放同步狀態的同時,會喚醒后繼節點,后繼節點在被喚醒后檢測自己的前驅節點是否是head節點,如果是則會通過自旋嘗試獲取同步狀態。
維護CLH的FIFO原則。該方法中節點自旋獲取同步狀態。
如下圖
shouldParkAfterFailedAcquire(Node pred, Node node)
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 { // 如果前驅狀態為0 或者 PROPAGATE 狀態, 那就把前驅狀態設置成SIGNAL,告訴它獲取資源后通知下自己。 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
此方法檢測自己前驅節點是否時head節點,如果是則嘗試獲取同步狀態,不是則再次回到同步隊列中找到一個舒適地方(也就是找到一個waitStatus > 0 的節點,排在他后面繼續等待)休息,并告訴前驅節點釋放同步狀態或者被中斷后通知自己下(compareAndSetWaitStatus(pred, ws, Node.SIGNAL))。
注意:在此查找一個舒適區域休息(waitStatus > 0 的節點)時那些不符合條件的節點會形成了一個無效鏈,等待GC回收。
private final boolean parkAndCheckInterrupt() { // 調用park方法是線程進入waiting 狀態 LockSupport.park(this); //如果被喚醒查看是不是被中斷狀態 return Thread.interrupted(); }
最后調用park方法使節點中線程進入wating狀態,等待被unpark()喚醒。
小結
請求線程首先調用tryAcquire(arg) 方法嘗試獲取同步狀態,成功則直接返回。
如果失敗:
構造一個獨占式節點Node.EXCLUSIVE
addWaiter(Node.EXCLUSIVE) 將該節點嘗試快速加入到隊列尾部,成功則直接返回該節點,失敗則調用enq(final Node node)方法利用自旋CAS將該節點加入到隊列尾部 。
調用acquireQueued(final Node, int arg) 方法找到一個舒適的休息區,并通知前驅節點在釋放同步狀態或者被中斷后喚醒自己從新嘗試獲取同步狀態。
最后如果節點線程在等待時被中斷,則將中斷補上selfInterrupt()
到這獨占式獲取共享狀態已經聊完了,下面我們一起來看看釋放共享狀態的過程。
release(int arg)
獨占式釋放共享資源的頂級入口release(int arg) 方法,徹底釋放共享狀態(state = 0)并喚醒其后繼節點來獲取共享狀態。
方法源碼
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) // 喚醒head節點的后繼節點。 unparkSuccessor(h); return true; } return false; }
源碼分析
tryRelease(arg)
protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); }
tryRelease(int arg) 和 tryAcquire(arg) 語義基本相同,留給子類去實現。
unparkSuccessor(h)
private void unparkSuccessor(Node node) { // 獲取當前節點的等待狀態 int ws = node.waitStatus; if (ws < 0) // 如果節點狀態小于0 (),將其狀態設置為0 compareAndSetWaitStatus(node, ws, 0); // 獲取其下一個需要喚醒的節點 Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; // 如果下一個節點為null,或者等待狀態大于0(被取消的狀態)繼續往下查找 直到等待狀態小于等于0的節點 for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) // 喚醒該節點等待線程 LockSupport.unpark(s.thread); }
小結
首先調用實現者的tryRelease(),失敗則返回false
成功則找到下一個有效的節點并喚醒它。
到這獨占式獲取同步和釋放同步狀態的源碼已經分析完了。 有沒有懵尼? 懵了也別怕最后我們再來張流程圖幫助大家理解。
結合上面源碼分析,應該對AQS獨占式獲取和釋放共享狀態的源碼有所了解了吧。
分析了獨占式同步狀態的獲取和釋放過程,適當做下總結: 在獲取同步狀態時,同步器維持一個同步隊列,獲取狀態失敗的線程都會加入到隊列中并在隊列中進行自旋,出列(或者停止自旋的)的條件是前驅節點為頭節點且成功獲取了同步狀態。在釋放同步狀態時,同步器調用tryRelease(int arg)方法釋放同步狀態,然后喚醒頭節點的后繼節點。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/74249.html
摘要:主要講解方法共享式獲取同步狀態,返回值表示獲取成功,反之則失敗。源碼分析同步器的和方法請求共享鎖的入口當并且時才去才獲取資源獲取鎖以共享不可中斷模式獲取鎖將當前線程一共享方式構建成節點并將其加入到同步隊列的尾部。 一、寫在前面 上篇給大家聊了獨占式的源碼,具體參見《J.U.C|AQS獨占式源碼分析》 這一章我們繼續在AQS的源碼世界中遨游,解讀共享式同步狀態的獲取和釋放。 二、什么是...
摘要:二什么是同步隊列同步隊列一個雙向隊列,隊列中每個節點等待前驅節點釋放共享狀態鎖被喚醒就可以了。三入列操作如上圖了解了同步隊列的結構,我們在分析其入列操作在簡單不過。 一、寫在前面 在上篇我們聊到AQS的原理,具體參見《J.U.C|AQS原理》。 這篇我們來給大家聊聊AQS中核心同步隊列(CLH)。 二、什么是同步隊列(CLH) 同步隊列 一個FIFO雙向隊列,隊列中每個節點等待前驅...
摘要:二什么是重入鎖可重入鎖,顧名思義,支持重新進入的鎖,其表示該鎖能支持一個線程對資源的重復加鎖。將由最近成功獲得鎖,并且還沒有釋放該鎖的線程所擁有。可以使用和方法來檢查此情況是否發生。 一、寫在前面 前幾篇我們具體的聊了AQS原理以及底層源碼的實現,具體參見 《J.U.C|一文搞懂AQS》《J.U.C|同步隊列(CLH)》《J.U.C|AQS獨占式源碼分析》《J.U.C|AQS共享式源...
摘要:公平鎖阻塞隊列前邊有線程,要去后邊排隊,簡單來說滾后邊等著去。非公平鎖不管是否有線程排隊,先槍鎖基于實現的可重入鎖實現類。 AQS原理介紹: AQS (Abstra...
摘要:簡介抽象隊列同步器,以下簡稱出現在中,由大師所創作。獲取成功則返回,獲取失敗,線程進入同步隊列等待。響應中斷版的超時響應中斷版的共享式獲取同步狀態,同一時刻可能會有多個線程獲得同步狀態。 1.簡介 AbstractQueuedSynchronizer (抽象隊列同步器,以下簡稱 AQS)出現在 JDK 1.5 中,由大師 Doug Lea 所創作。AQS 是很多同步器的基礎框架,比如 ...
閱讀 2772·2021-11-19 11:30
閱讀 3057·2021-11-15 11:39
閱讀 1782·2021-08-03 14:03
閱讀 1984·2019-08-30 14:18
閱讀 2042·2019-08-30 11:16
閱讀 2148·2019-08-29 17:23
閱讀 2596·2019-08-28 18:06
閱讀 2532·2019-08-26 12:22