先推薦篇寫AQS的不錯的文章:《從ReentrantLock的實現(xiàn)看AQS的原理及應(yīng)用》河狐、《一文了解AQS(AbstractQueuedSynchronizer)》困檩、《AQS及其組件的核心原理》
AQS 的核心作用是:定義了一套多線程訪問共享資源的同步模板庆猫,解決了實現(xiàn)同步器時涉及的大量細節(jié)問題(線程阻塞等待喚醒的機制铡俐,無鎖狀態(tài)加鎖,有鎖狀態(tài)將線程放在等待隊列排隊獲取鎖)絮供,能夠極大地減少實現(xiàn)工作;這種有效等待,相比于其他死等待或休眠機制崩泡、一方面減少了CPU空耗,另一方面機制能力很強猬膨,可以滿足非常多并發(fā)控制的場景角撞。
//類關(guān)系圖
//重要的屬性
方法和屬性值 | 含義 |
---|---|
waitStatus | 當前節(jié)點在隊列中的狀態(tài) |
prev | 前驅(qū)指針 |
next | 后繼指針 |
thread | 表示處于該節(jié)點的線程 |
nextWaiter | 指向下一個處于CONDITION狀態(tài)的節(jié)點 |
predecessor | 返回前驅(qū)節(jié)點,沒有的話拋出npe |
waitStatus有下面幾個枚舉值:
方法和屬性值 | 含義 |
---|---|
CANCELLED | 為1勃痴,表示線程獲取鎖的請求已經(jīng)取消了 |
SIGNAL | 為-1谒所,表示線程已經(jīng)準備好了,就等資源釋放了 |
CONDITION | 為-2沛申,表示節(jié)點在等待隊列中劣领,節(jié)點線程等待喚醒 |
PROPAGATE | 為-3,當前線程處在SHARED情況下铁材,該字段才會使用 |
0 | 當一個Node被初始化的時候的默認值 |
//非公平鎖
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
//上鎖的方法尖淘,final表明該方法不能被重寫
final void lock() {
//調(diào)用aqs的cas方法來修改state,這個為什么是1著觉,重入鎖:每次會對state+1
//這兒是直接cas獲取村生,沒有判斷可重入性,該方法只會適用于首次獲取鎖饼丘,如果獲取失敗趁桃,會走下面的acquire(1)
if (compareAndSetState(0, 1))
//記錄那個線程獲得了鎖,用于后面重入比對線程
setExclusiveOwnerThread(Thread.currentThread());
else
//獲取失敗會再次嘗試獲取
acquire(1);
}
//上面的acquire(1)調(diào)用的是syn類acquire(int arg)方法葬毫,然后又會回調(diào)到子類的這個方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
AQS類的方法,該方法非常重要镇辉,是獨占鎖獲取鎖的頂層入庫
public final void acquire(int arg) {
//如上面說的,先調(diào)用子類獲取贴捡,若失敗忽肛,就會添加到隊列
//這里面涉及了四個方法tryAcquire;addWaiter烂斋;acquireQueued屹逛;selfInterrupt
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
selfInterrupt();
}
}
1.tryAcquire,上面說了最終會調(diào)用子類的nonfairTryAcquire實現(xiàn)汛骂,代碼如下(嘗試直接去獲取資源罕模,如果成功則直接返回(這里體現(xiàn)了非公平鎖,每個線程獲取鎖時會嘗試直接搶占加塞一次帘瞭,而CLH隊列中可能還有別的線程在等待)淑掌;)
//#java.util.concurrent.locks.ReentrantLock
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//獲取aqs類的state狀態(tài); 這兒有個知識點就是volatile關(guān)鍵字,只>有可見性和禁止重排序蝶念,沒有原子性抛腕,這也是為啥不能保證線程安全的>>原因
int c = getState();
//再次判斷state狀態(tài)芋绸,雖然第一次獲取失敗了,到這兒可能已經(jīng)被釋放了担敌,所以再次判斷
if (c == 0) {
//這個if里面的類容和第一次獲取鎖一致
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//不等于0的時候摔敛,說明已經(jīng)有線程線程持有資源了,進行重入性判斷全封,就是比對線程對象是否相同
else if (current == getExclusiveOwnerThread()) {
//重入對象的計數(shù)+1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//更新state
setState(nextc);
return true;
}
return false;
}
2.addWaiter方法:將該線程加入等待隊列的尾部马昙,并標記為獨占模式
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//將尾結(jié)點賦給pred,目的是騰位置刹悴,后面方便掛最新的mode到CLH隊尾
Node pred = tail;
//說明CLH中有其他node節(jié)點在排隊行楞,pre == null說明等待隊列中沒有元素
if (pred != null) {
//把新的node節(jié)點的prev指向之前的tail,這樣就把新節(jié)點的前驅(qū)指針掛在之前的隊尾了颂跨,相當于掛了一半上去了
//最新理解:為啥先掛當前節(jié)點的前驅(qū)敢伸,而不是直接node = tail.next;因為如果要開始就先掛后驅(qū)恒削,那就需要把接下來的compareAndSetTail(pred, node)和 pred.next = node;設(shè)置為原子操作才行池颈,不然在多線程環(huán)境下數(shù)據(jù)混亂
node.prev = pred;
// 另一半也需要掛上去,需要把當前的隊尾更新(即更新tail指針)钓丰,涉及并發(fā)躯砰;如果修改失敗說明被別的線程修改了;
// 如果當前節(jié)點node設(shè)置為隊尾失敗携丁,那就只掛了一半上去琢歇,實際上的隊尾節(jié)點是并發(fā)的另一個線程
if (compareAndSetTail(pred, node)) {
// 非原子性操作,由于之前是先掛的前驅(qū)指針梦鉴,所以這句話運行完李茫,當前node就成功入隊,且隊尾tail為當前節(jié)點肥橙,這也是為啥后面喚醒node需要從尾部遍歷的原因之一
pred.next = node;
return node;
}
}
// 什么時候到這兒魄宏,tail節(jié)點為空,或者cas設(shè)置tail失敗
//最新理解:先說問題存筏,為什么要有這個方法:因為掛另一半上去的時候宠互,由于并發(fā)失敗了,當前節(jié)點需要最終掛上去椭坚,就通過該方法自旋予跌,不斷CAS重試,直到掛上去(即成為尾部節(jié)點)
enq(node);
return node;
}
private Node enq(final Node node) {
//自旋善茎,直到入隊尾成功
for (;;) {
Node t = tail;
// tail為空券册,說明當前隊列里面沒有元素
if (t == null) { // Must initialize
// 初始化的頭結(jié)點,并不是當前線程節(jié)點,而是調(diào)用了無參構(gòu)造函數(shù)的節(jié)點
if (compareAndSetHead(new Node()))
tail = head;
} else {
//該操作和上一個方法一致汁掠,加到隊尾
node.prev = t;
// 更新tail指針略吨,指向當前node
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
3.acquireQueued:使線程阻塞在等待隊列中獲取資源,一直獲取到資源后才返回考阱。如果在整個等待過程中被中斷過,則返回true鞠苟,否則返回false(總的來說乞榨,一個線程獲取鎖失敗了,被放入等待隊列当娱,acquireQueued會把放入隊列中的線程不斷去獲取鎖吃既,直到獲取成功或者不再需要獲取(中斷))
final boolean acquireQueued(final Node node, int arg) {
//是否拿到資源的標記跨细;默認失敗
boolean failed = true;
try {
//線程在等待過程中是否中斷的標記
boolean interrupted = false;
// 自旋鹦倚,什么時候自旋結(jié)束?“前置節(jié)點是頭結(jié)點冀惭,且當前線程獲取鎖成功”
// 一直自旋會導致cpu資源浪費震叙,所以會對當前節(jié)點的前置節(jié)點進行狀態(tài)判斷覺得是否要將線程掛起,是否掛起要看shouldParkAfterFailedAcquire方法
for (;;) {
// 獲取前驅(qū)散休,該方法里面就是把prev給返回
final Node p = node.predecessor();
// 如果前驅(qū)p是head媒楼,即該結(jié)點在隊列中第二位,由于CLH隊列的頭結(jié)點是虛節(jié)點戚丸,為空划址,沒有實際的線程信息,所以當前node就是正兒八經(jīng)的首位
//所以跳出循環(huán)的條件:前置節(jié)點是頭結(jié)點限府,且當前線程獲取鎖成功
if (p == head && tryAcquire(arg)) {
// 獲取到資源后夺颤,將head指向該結(jié)點;該方法里面將node賦值給head,并把該節(jié)點的線程和prev置空
// 相當于指針移動到當前node胁勺,本質(zhì)就是將當前節(jié)點置為虛節(jié)點世澜,把之前的虛節(jié)點引用斷開)
// 為什么不是直接remove當前節(jié)點,而是把head指向當前節(jié)點姻几,這個操作的目的是什么
// 1.因為當前節(jié)點的里面的waitStatus 后面還有用宜狐,如果移除,需要找個地方保存下來蛇捌;2.如果移除當前節(jié)點抚恒,需要把head和當前節(jié)點的后繼連起來,需要變更4個前驅(qū)后繼的引用络拌,但是直接把當前節(jié)點設(shè)置成頭結(jié)點俭驮,只有三次。
setHead(node);
p.next = null; // help GC
//改變標志位,成功獲取資源
failed = false;
return interrupted;
}
// 執(zhí)行到這兒說明有兩個可能混萝,1:p為頭節(jié)點且當前沒有獲取到鎖(可能是非公平鎖被搶占了)2:p不為頭結(jié)點遗遵;
// 這個時候就要判斷當前node是否要被阻塞(被阻塞條件:前驅(qū)節(jié)點的waitStatus為-1),防止無限循環(huán)浪費資源逸嘀。那怎么判斷是否需要阻塞呢车要,就要看下面這個方法了
// parkAndCheckInterrupt主要用于掛起當前線程,阻塞調(diào)用棧崭倘,返回當前線程的中斷狀態(tài)翼岁。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
4.shouldParkAfterFailedAcquire:靠前驅(qū)節(jié)點判斷當前線程是否應(yīng)該被阻塞。白話文就是你前面?zhèn)€哥們都在排隊等待被喚醒司光,你不阻塞琅坡,在哪兒瞎折騰啥。(ws == Node.SIGNAL(-1))將會被掛起阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 獲取前驅(qū)結(jié)點的節(jié)點狀態(tài)
int ws = pred.waitStatus;
// 說明前驅(qū)結(jié)點處于待喚醒狀態(tài)残家,當前節(jié)點node肯定可以安全的阻塞榆俺,所以返回true
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
// 通過枚舉值我們知道waitStatus>0僅僅只有取消狀態(tài)(該邏輯就是優(yōu)化隊列,剔除取消的節(jié)點)
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
// 循環(huán)向前查找取消節(jié)點坞淮,把取消節(jié)點從隊列中剔除
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.
*/
//運行到這里茴晋,說明前驅(qū)節(jié)點處于0、CONDITION或者PROPAGATE狀態(tài)下
//此時該節(jié)點需要被置為SIGNAL狀態(tài)碾盐,等待被喚醒晃跺。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
5.取消node節(jié)點
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
// 設(shè)置該節(jié)點不關(guān)聯(lián)任何線程
node.thread = null;
// Skip cancelled predecessors
// 跳過取消的前驅(qū)節(jié)點
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
// 執(zhí)行到這一步,說明已經(jīng)優(yōu)化隊列了毫玖,把取消的前驅(qū)節(jié)點清除出隊
// 將未取消的前驅(qū)節(jié)點的后繼節(jié)點拿到
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
//根據(jù)當前節(jié)點存在的位置掀虎,可能存在三種可能
//1.當前節(jié)點是尾節(jié)點。
//2. 當前節(jié)點是Head的后繼節(jié)點付枫。
//3. 當前節(jié)點不是Head的后繼節(jié)點烹玉,也不是尾節(jié)點。
// If we are the tail, remove ourselves.
// 如果當前節(jié)點是尾節(jié)點阐滩,將從后往前的第一個非取消狀態(tài)的節(jié)點設(shè)置為尾節(jié)點
// 更新失敗的話二打,則進入else,如果更新成功掂榔,將tail的后繼節(jié)點設(shè)置為null
if (node == tail && compareAndSetTail(node, pred)) {
// 將前驅(qū)節(jié)點的next指向null
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
// 如果前驅(qū)節(jié)點不是head節(jié)點(即node不在正兒八經(jīng)的首位)继效,1:判斷前驅(qū)節(jié)點是否為SIGNAL,2:如果不是装获,則把前驅(qū)節(jié)點設(shè)置為SINGAL看是否成功
// 如果1和2中有一個為true瑞信,再判斷當前節(jié)點的線程是否為null
// 如果上述條件都滿足,把當前節(jié)點的前驅(qū)節(jié)點的后繼指針指向當前節(jié)點的后繼節(jié)點
//(pred != head)說明前驅(qū)不是head穴豫,當前要取消的node位于隊列中
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
// 把前驅(qū)的preNext指向node的next凡简,這樣node就不在隊列里面了
compareAndSetNext(pred, predNext, next);
} else {
// 如果當前節(jié)點是head的后繼節(jié)點逼友,或者上述條件不滿足,那就喚醒當前節(jié)點的后繼節(jié)點
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
Q:某個線程獲取鎖失敗的后續(xù)流程是什么呢秤涩?
A:看本文的流程圖:嘗試CAS再獲取一次帜乞,失敗后自旋到同步隊列尾部,然后自旋去獲取鎖(涉及清除取消的節(jié)點筐眷,進入阻塞掛起)黎烈。Q:既然說到了排隊等候機制,那么就一定會有某種隊列形成浊竟,這樣的隊列是什么數(shù)據(jù)結(jié)構(gòu)呢怨喘?
A:是CLH變體的FIFO雙端隊列。Q:處于排隊等候機制中的線程振定,什么時候可以有機會獲取鎖呢?
A:前置節(jié)點為頭結(jié)點肉拓。Q:那獲取鎖成功之后不是應(yīng)該把當前節(jié)點直接remove移除掉后频,為什么是把 head 指向成當前節(jié)點,這個是什么操作呢暖途?
A:當前節(jié)點獲取鎖成功(不再需要繼續(xù)for 循環(huán)或阻塞等待鎖)卑惜,可以開始執(zhí)行后續(xù)的業(yè)務(wù)邏輯了。這時候不是將當前節(jié)點從隊列移除驻售,而是設(shè)置為頭結(jié)點露久,二方面原因:
因為當前節(jié)點的里面的waitStatus 后面還有用,如果移除欺栗,需要找個地方保存下來毫痕;
如果移除當前節(jié)點,需要把head和當前節(jié)點的后繼連起來迟几,需要變更4個前驅(qū)后繼的引用消请,但是直接把當前節(jié)點設(shè)置成頭結(jié)點,只有三次类腮。
Q:如果處于排隊等候機制中的線程一直無法獲取鎖臊泰,需要一直等待么?還是有別的策略來解決這一問題蚜枢?
A:線程所在節(jié)點的狀態(tài)會變成取消狀態(tài)缸逃,取消狀態(tài)的節(jié)點會從隊列中釋放。
Q:Lock函數(shù)通過Acquire方法進行加鎖厂抽,但是具體是如何加鎖的呢需频?
A:AQS的Acquire會調(diào)用tryAcquire方法,tryAcquire由各個自定義同步器實現(xiàn)修肠,通過tryAcquire完成加鎖過程贺辰。
Q:shouldParkAfterFailedAcquire中取消節(jié)點是怎么生成的呢?什么時候會把一個節(jié)點的waitStatus設(shè)置為-1?
A:是在cancelAcquire中產(chǎn)生的饲化;如果當前節(jié)點不是head的后繼節(jié)點莽鸭,1:判斷當前節(jié)點前驅(qū)節(jié)點的是否為SIGNAL,2:如果不是吃靠,則把前驅(qū)節(jié)點設(shè)置為SINGAL看是否成功
Q:被掛起阻塞的線程是什么時候進行恢復(fù)的硫眨?
A:首先被喚醒調(diào)用的方法是unparkSuccessor(Node node) ;線程喚醒發(fā)生在取消請求時cancelAcquire()巢块,或釋放鎖時對unparkSuccessor()的調(diào)用礁阁。
Q:喚醒節(jié)點的邏輯是從后尾部往前遍歷找到最前的一個處于正常阻塞狀態(tài)的結(jié)點,為什么需要從后往前遍歷
A:如果是從前往后找族奢,由于極端情況下addWaiter入隊的非原子操作和CANCELLED節(jié)點產(chǎn)生過程中斷開Next指針的操作姥闭,可能會導致無法遍歷所有的節(jié)點。
在enq方法插入新節(jié)點時越走,可能存在舊尾節(jié)點的next指針還未指向新節(jié)點的情況;
在shouldParkAfterFailedAcquire方法中棚品,當移除CANCEL狀態(tài)的節(jié)點時,也存在next指針還未指向后續(xù)節(jié)點的情況
Q:對ReentrantLock非公平性的理解
A:首次去獲取鎖及獲取鎖失敗重試都是非公平的廊敌,都會和隊列里面的競爭铜跑,當加入隊列后,相對于整個FIFO隊列的node節(jié)點來說骡澈,又是公平的锅纺,但在隊列里面獲取鎖的時候又會同其他線程競爭,又體現(xiàn)了非公平性
Q:ReentrantLock與synchronized的區(qū)別
A: ReentrantLock具有以下優(yōu)勢
- ReentrantLock有公平鎖
- ReentrantLock嘗試加鎖肋殴,獲取鎖帶超時時間
- ReentrantLock獲取鎖響應(yīng)中斷
- 底層實現(xiàn):synchronized 是 JVM層面的鎖囤锉,是Java關(guān)鍵字,通過monitor對象來完成(monitorenter與monitorexit)疼电,ReentrantLock 是從jdk1.5以來(java.util.concurrent.locks.Lock)提供的API層面的鎖
- 鎖的對象:synchronzied 鎖的是對象嚼锄,鎖是保存在對象頭里面的,根據(jù)對象頭數(shù)據(jù)來標識是否有線程獲得鎖/爭搶鎖蔽豺;ReentrantLock 是根據(jù)volatile 變量 state 標識鎖的獲得/爭搶区丑。
- 實現(xiàn)機制:synchronized 的實現(xiàn)涉及到鎖的升級,具體為無鎖修陡、偏向鎖沧侥、自旋鎖、向內(nèi)核態(tài)申請重量級鎖魄鸦,ReentrantLock實現(xiàn)則是通過利用CAS(CompareAndSwap)自旋機制保證線程操作的原子性和volatile 保證數(shù)據(jù)可見性以實現(xiàn)鎖的功能宴杀。
- 釋放鎖方式:synchronized 不需要用戶去手動釋放鎖,synchronized 代碼執(zhí)行完后系統(tǒng)會自動釋放鎖拾因;ReentrantLock 需要用戶去手動釋放鎖旺罢,如果沒有手動釋放鎖旷余,就可能導致死鎖現(xiàn)象。一般通過lock() 和unlock() 方法配合 try/finally 語句塊來完成扁达。
- 帶條件的鎖:synchronized不能綁定條件正卧;ReentrantLock 可以綁定Condition 結(jié)合await()/singal() 方法實現(xiàn)線程的精準喚醒,而不是像synchronized通過Object類的wait()/notify()/notifyAll() 方法要么隨機喚醒一個線程要么喚醒全部線程跪解。
總結(jié):
ReentrantLock 與 synchronized的區(qū)別:
- synchronized 屬于非公平鎖炉旷,ReentrantLock 支持公平鎖、非公平鎖叉讥;
- synchronized 不支持嘗試加鎖窘行,ReentrantLock 支持嘗試加鎖,支持帶超時時間的嘗試加鎖图仓;
- synchronized 不支持響應(yīng)中斷的獲取鎖罐盔,ReentrantLock 提供了響應(yīng)中斷的加鎖方法;
- synchronized 不支持帶條件救崔,ReentrantLock支持翘骂;
- 其他底層實現(xiàn)上、實現(xiàn)機制上帚豪、鎖的對象上役听、釋放鎖的方式上也有區(qū)別底洗。