摘要:我們可以將的作用理解為在多線程的環境下保證線程等待獲取鎖添加進入隊列以及線程獲取鎖,并隊列中出去都是線程安全的。是如何做到線程安全的主要是通過死循環以及狀態值,來做到線程安全。
1、什么是aqs
aqs是一個FIFO的雙向鏈表隊列。aqs將等待獲取鎖的線程封裝成結點,放在隊列中。
我們可以將aqs的作用理解為在多線程的環境下保證線程等待獲取鎖(添加進入隊列)以及線程獲取鎖,并隊列中出去都是線程安全的。
更簡單的可以理解為aqs為了保證在多線程的環境下入隊列和出隊列的線程安全性提供了一個基本功能框架。
2、aqs是如何做到線程安全的
aqs主要是通過cas + 死循環以及state狀態值,來做到線程安全。
3、aqs為什么會被設計為FIFO雙向鏈表隊列(以下是個人理解)
①aqs的鎖實現,包含公平鎖和非公平鎖。為了實現公平鎖,必須使用隊列來保證獲取鎖的順序(入隊列的順序)
②用鏈表的方式,主要是因為,操作更多是刪除與增加。鏈表時間復雜度O(1)的效率會比數組O(n)的低。
③用雙向隊列的原因是,aqs的設計思想,或則說為了解決羊群效應(為了爭奪鎖,大量線程同時被喚醒)。每個結點(線程)只需要關心自己的前一個結點的狀態(后續會說),線程喚醒也只喚醒隊頭等待線程。
請參考 http://www.importnew.com/2400...
4、aqs是如何提供一個基礎框架的
aqs 通過模板設計進行提供的,實現類只需實現特定的方法即可。
以下是aqs的模板方法
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } 。。。 其他的省略了 protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); } protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
tryAcquire(int arg),tryRelease(int arg) 是我們要實現的模板方法,當然還有分享鎖的,這里只介紹了獨占鎖的。
5、從源碼角度剖析aqs。aqs是如何通過雙向鏈表隊列,cas,state狀態值,以及結點狀態來保證入隊列出隊列的線程安全的!
注:以下只介紹獨占式的不公平鎖
①aqs 如何獲取鎖?
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
tryAcquire(arg) 內部調用了nonfairTryAcquire(int acquires)
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 鎖未被獲取 // cas(自旋) 獲取鎖,并修改state 狀態值 if (compareAndSetState(0, acquires)) { // 設置當前占有的線程 setExclusiveOwnerThread(current); return true; } } // 重入 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
解釋:利用cas自旋式的獲取鎖。
②aqs 獲取鎖失敗,如何處理?
在看代碼前,先解釋一下:將當前線程包裝成Node結點,并插入同步隊列中,并用CAS形式嘗試獲取鎖,獲取失敗,則掛起當前線程(以上只是說了大概)
先看第1個方法(將當前線程包裝成Node結點,并插入同步隊列)
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { // 尾節點不為空 node.prev = pred; // 用 CAS 將當前線程插入隊尾 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 尾節點為空,說明當前隊列還是空的,需要初始化 enq(node); return node; } private Node enq(final Node node) { // 死循環 for (;;) { // 初始化 Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { // 這里主要是擔心有多個線程同時進到enq(final Node node) 方法 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
解釋:隊列若為空,先初始化,不為空,用 CAS 將當前結點插入到隊尾
再看第二個方法final boolean acquireQueued(final Node node, int arg);
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 GC failed = false; return interrupted; } // 前置結點不是頭結點 或者 前置結點是頭結點但是嘗試獲取鎖失敗 // 則,應當將當前線程掛起(畢竟不能一直死循環獲取吧~) if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } } private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { // 當前線程的前置節點的狀態?。?! // 第waitStatus 初始化值為0, // 也因此當第1次進到這個方法時,會將前置結點的狀態置為 Node.SIGNAL。 // 第 2次進來的時候,前置節點的waitStatus的狀態就為 Node.SIGNAL)。 // 也就是說。aqs 只會讓你嘗試2次,都失敗后,就會被掛起 int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don"t park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } // 線程被掛起調用該方法??! private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }
讓我們總結一下,以及再回顧一下,為什么aqs會被設計為雙向鏈表隊列。
aqs為了保證結點(即線程)的入隊列的安全。采用了CAS 以及死循環的方式(從代碼中可看到,處處使用CAS)。
上面有說到,一個線程是否該被喚醒或者其他操作,只需要看前置結點的狀態即可。從shouldParkAfterFailedAcquire() 方法就可以看出這個設計。當前線程該做什么操作,是看前置結點的狀態的。
③aqs如何釋放鎖
看代碼前,先解釋一下,aqs是如何做的。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; }
①ReentratLock 是如何實現鎖的釋放的
注:這里看的是ReentrantLock的實現
protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
解釋:設置 state 的狀態,如果 state == 0, 那么說明鎖被釋放了。否則鎖還未被釋放(鎖重入!)
②aqs 如何喚醒其他結點
private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ // 清除狀態,還記得等待的線程會把前置節點的狀態置為 Node.SIGNAL(-1)嗎 int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ // 正常情況下,下一個結點就是被喚醒的節點。 // 但是如果下一個結點為null, 或者是被取消的 // 那么從尾節點向前查找一個未被取消的節點喚醒。 Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) // 喚醒 LockSupport.unpark(s.thread); }
release的釋放比較簡單。還是可以看到,aqs被設計成雙向鏈表隊列的好處!!!
看源代碼,不能一下子就扎進去看,要先明白個大概,為什么看源代碼?還不是為了學習作者是如何設計的。細節無論誰都記不清,最主要的是知道一個整體的流程,關鍵的代碼!畢竟優秀的開源項目這么多,難道每行代碼都看??
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76678.html
摘要:關于,最后有兩點規律需要注意當的等待隊列隊首結點是共享結點,說明當前寫鎖被占用,當寫鎖釋放時,會以傳播的方式喚醒頭結點之后緊鄰的各個共享結點。當的等待隊列隊首結點是獨占結點,說明當前讀鎖被使用,當讀鎖釋放歸零后,會喚醒隊首的獨占結點。 showImg(https://segmentfault.com/img/remote/1460000016012293); 本文首發于一世流云的專欄:...
摘要:為了避免一篇文章的篇幅過長,于是一些比較大的主題就都分成幾篇來講了,這篇文章是筆者所有文章的目錄,將會持續更新,以給大家一個查看系列文章的入口。 前言 大家好,筆者是今年才開始寫博客的,寫作的初衷主要是想記錄和分享自己的學習經歷。因為寫作的時候發現,為了弄懂一個知識,不得不先去了解另外一些知識,這樣以來,為了說明一個問題,就要把一系列知識都了解一遍,寫出來的文章就特別長。 為了避免一篇...
摘要:在時,引入了包,該包中的大多數同步器都是基于來構建的??蚣芴峁┝艘惶淄ㄓ玫臋C制來管理同步狀態阻塞喚醒線程管理等待隊列。指針用于在結點線程被取消時,讓當前結點的前驅直接指向當前結點的后驅完成出隊動作。 showImg(https://segmentfault.com/img/remote/1460000016012438); 本文首發于一世流云的專欄:https://segmentfau...
摘要:當線程使用完共享資源后,可以歸還許可,以供其它需要的線程使用。所以,并不會阻塞調用線程。立即減少指定數目的可用許可數。方法用于將可用許可數清零,并返回清零前的許可數六的類接口聲明類聲明構造器接口聲明 showImg(https://segmentfault.com/img/bVbfdnC?w=1920&h=1200); 本文首發于一世流云的專欄:https://segmentfault...
閱讀 958·2022-06-21 15:13
閱讀 1848·2021-10-20 13:48
閱讀 1029·2021-09-22 15:47
閱讀 1365·2019-08-30 15:55
閱讀 3112·2019-08-30 15:53
閱讀 520·2019-08-29 12:33
閱讀 712·2019-08-28 18:15
閱讀 3458·2019-08-26 13:58