深入剖析AQS和CAS,看了都說好
掃描二維碼
隨時隨地手機看文章
AQS簡介
AQS(AbstractQueuedSynchronizer)為「抽象隊列同步器」,簡單的說「AQS就是一個抽象類」,抽象類就是
AbstractQueuedSynchronizer,沒有實現(xiàn)任何的接口,「僅僅定義了同步狀態(tài)(state)的獲取和釋放的方法」。
它提供了一個「FIFO隊列」,多線程競爭資源的時候,沒有競爭到的線程就會進(jìn)入隊列中進(jìn)行等待,并且「定義了一套多線程訪問共享資源的同步框架」。
在AQS中的鎖類型有兩種:分別是「Exclusive(獨占鎖)「和」Share(共享鎖)」。
「獨占鎖」就是「每次都只有一個線程運行」,如
ReentrantLock。
「共享鎖」就是「同時可以多個線程運行」,如
Semaphore、
CountDownLatch、ReentrantReadWriteLock。
AQS源碼分析
AQS源碼分析
在AQS的源碼可以看到對于「state共享變量」,使用「volatile關(guān)鍵字」進(jìn)行修飾,從而「保證了可見性」。從上面的源碼中可以看出,對于state的修改操作提供了
setState和
compareAndSetState,「那么為什么要提供這兩個對state的修改呢?」
因為
compareAndSetState方法「通常使用在獲取到鎖之前」,當(dāng)前線程不是鎖持有者,對于state的修改可能存在線程安全問題,所以「需要保證對state修改的原子性操作」。
而
setState方法通常用于當(dāng)前正持有鎖的線程對「state共享變量」進(jìn)行修改,因為不存在競爭,是線程安全的,所以沒必要使用CAS操作。
分析了AQS的源碼的實現(xiàn),接下來我們看看AQS的實現(xiàn)的原理。這里AQS的實現(xiàn)源碼和理論都會比較簡單,因為還沒有涉及到具體的實現(xiàn)類。
AQS實現(xiàn)原理
AQS實現(xiàn)原理
上面說到AQS中維護(hù)了一個「FIFO隊列」,并且「該隊列是一個雙向鏈表」,鏈表中的每一個節(jié)點為「Node節(jié)點」,「Node類是AbstractQueuedSynchronizer中的一個內(nèi)部類」。
讓我們來看看AQS中Node內(nèi)部類的源碼,這樣有助于我們能夠?qū)QS的內(nèi)部的實現(xiàn)更加的清晰:
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
可以看到上面的Node類比較簡單,只是對于每個Node節(jié)點擁有的屬性進(jìn)行維護(hù),在Node內(nèi)部類中最重要的基本構(gòu)成就是這幾個:
volatile Node prev;
volatile Node next;
volatile Thread thread;
「根據(jù)源碼的分析和線程的競爭共享資源的原理」,關(guān)于AQS的實現(xiàn)原理,我這里畫了一張圖:在FIFO隊列中,「頭節(jié)點占有鎖」,也就是頭節(jié)點才是鎖的持有者,尾指針指向隊列的最后一個等待線程節(jié)點,除了頭節(jié)點和尾節(jié)點,節(jié)點之間都有「前驅(qū)指針」和「后繼指針」
在AQS中維護(hù)了一個「共享變量state」,標(biāo)識當(dāng)前的資源是否被線程持有,多線程競爭的時候,會去判斷state是否為0,嘗試的去把state修改為1
分析了AQS的源碼的實現(xiàn)和原理實現(xiàn),但是AQS里面具體是沒有做同步的具體實現(xiàn),如果要什么了解AQS的具體的實現(xiàn)原理,要需要看AQS的具體實現(xiàn)類,這邊就以ReentrantLock為例。
ReentrantLock實現(xiàn)原理
如果多線程在競爭共享資源時,「競爭失敗的線程就會添加入FIFO隊列的尾部」。
在ReentrantLock的的具體實現(xiàn)中,這邊以在ReentrantLock的非公平鎖的實現(xiàn)為例,因為公平鎖的實現(xiàn),之前已經(jīng)寫過一篇文章分析過了。
我們來看看新添加節(jié)點的源碼寫的實現(xiàn)邏輯:當(dāng)競爭鎖資源的線程失敗后直接進(jìn)入acquire(1)方法,從源碼中可以看出,acquire(1)的實現(xiàn)主要有這三步的邏輯:
-
tryAcquire(arg):嘗試再次獲取鎖。
-
addWaiter(Node.EXCLUSIVE):若是獲取鎖失敗,就會將當(dāng)前線程組裝成一個Node節(jié)點,進(jìn)行入隊操作。
-
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)):acquireQueued方法以addWaiter返回的頭節(jié)點作為參數(shù),內(nèi)部實現(xiàn)進(jìn)行鎖自旋,以及判斷是否應(yīng)該執(zhí)行線程掛起。
下面我們再來看看tryAcquire(arg)的源碼,從上面的看一看出arg的值為1,具體的實現(xiàn)源碼如下:從源碼的分析中可以看出,tryAcquire(arg)的實現(xiàn)也就是判斷state的值是否已經(jīng)被釋放,「若釋放則當(dāng)前線程就會CAS操作將state設(shè)置為1,若是沒有釋放,就會判斷是否可以進(jìn)行鎖的重入」。
分析完tryAcquire(arg)的實現(xiàn),來看看addWaiter:對于新加入的線程添加到雙向鏈表中使用尾插法。當(dāng)線程加入隊列中,主要進(jìn)行這幾步操作「新加入的節(jié)點prev指針指向原來的tail節(jié)點,原來的tail節(jié)點的next指針指向新加入的節(jié)點」,這個也就是常見的「雙向鏈表尾插法」的操作。
「最后把tail指向新加入的節(jié)點」,如此一來就完成了新加入節(jié)點的入隊操作,接下來我們接著分析源碼。
當(dāng)然這里的前提是「隊列中不為空」,若是為空的話,不會走上面的邏輯,而是走enq(node),進(jìn)行初始化節(jié)點,我們來看看enq(node)操作:執(zhí)行完上面的入對操作后,接著執(zhí)行acquireQueued方法,來看看它的具體實現(xiàn)源碼:從上上面的源碼中可以看出,涉及到「頭節(jié)點head的出隊」操作,并且將「當(dāng)前線程的node節(jié)點晉升為head節(jié)點」。
因為只有「頭節(jié)點才是鎖的持有者」,所以對于head節(jié)點的出隊操作,head的指向會隨時改變:會把「原來的頭節(jié)點進(jìn)行出隊操作,也就是把原來的頭節(jié)點next指針指向null,原來第二節(jié)點的prev指針指向null」。
最后把head指針指向第二節(jié)點,當(dāng)然thread2同時還會修改共享狀態(tài)變量state的值,如此一來就完成了鎖的釋放。
當(dāng)釋放完鎖之后,就會執(zhí)行shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()判斷當(dāng)前的線程是否應(yīng)該被掛起,我們來看看它的源碼實現(xiàn):在shouldParkAfterFailedAcquire中的實現(xiàn),當(dāng)前驅(qū)節(jié)點的狀態(tài)量waitStatus為SIGNAL的時候,就會掛起。通過上面的分析對于AQS的實現(xiàn)基本有比較清晰的認(rèn)識,主要是對實現(xiàn)類ReentrantLock的實現(xiàn)原理進(jìn)行深入的分析,并且是基于「非公平鎖」和「獨占鎖」的實現(xiàn)。
在AQS的底層維護(hù)了一個「FIFO隊列」,多線程競爭共享資源的時候,「失敗的線程會被添加入隊列中」,「非公平鎖」實現(xiàn)中,新加入的線程節(jié)點會「自旋」嘗試的獲取鎖。
分析完AQS我們來分析CAS,「那么什么是CAS呢?」
CAS簡介
在分析ReentrantLock的具體實現(xiàn)的源碼中,可以看出所有涉及設(shè)置共享變量的操作,都會指向CAS操作,保證原子性操作。
CAS(compare and swap)原語理解就是比較并交換的意思,「CAS是一種樂觀鎖的實現(xiàn)」。
在CAS的算法實現(xiàn)中有三個值:「更新的變量」、「舊的值」、「新值」。在修改共享資源時候,會與原值進(jìn)行比較,若是等于原值,就修改為新值。
于是在這里的算法實現(xiàn)下,即使不加鎖,也能保證數(shù)據(jù)的可見性,即使的發(fā)現(xiàn)數(shù)據(jù)是否被更改,若是數(shù)據(jù)已經(jīng)被更新則寫操作失敗。
但是CAS也會引發(fā)ABA的問題,「什么是ABA問題呢?」 不慌請聽我詳細(xì)道來
ABA問題
ABA問題就是假如有兩個線程,同一時間讀取一個共享變量state=1,此時兩個線程都已經(jīng)將state的副本賦值到自己的工作內(nèi)存中。
當(dāng)線程一對state修改state=state+1,并且寫入到主存中,然后線程一又對state=state-1寫入到主存,此時主存的state是變化了兩次,只不過又變回了原來的值。
那么此時線程二修改state的時候就會修改成功,這就是ABA問題。對于「ABA問題的解決方案就是加版本號(version)」,每次進(jìn)行比較的時候,也會比較版本號。
因為版本版是只增不減,比如以時間作為版本號,每一時刻的時間都不一樣,這樣就能避免ABA的問題。
CAS性能分析
相對于「synchronized的阻塞算法」的實現(xiàn),「CAS采用的是樂觀鎖的非阻塞算法」的實現(xiàn),一般CPU在進(jìn)行線程的上下文切換的時間比執(zhí)行CPU的指令集的時間長,所以CAS操作在性能上也有了很大的提升。
但是所有的算法都是沒有最完美的,在執(zhí)行CAS的操作中,沒有更新成功的就會自旋,這樣也會消耗CPU的資源,對于CPU來說是不友好的。
特別推薦一個分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長按關(guān)注一下:
長按訂閱更多精彩▼
如有收獲,點個在看,誠摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!