首頁>技術>

CAS(Compare And Swap)原理分析

字面意思是比較和交換,先看看下面場景(A 和 B 執行緒同時執行下面的程式碼):

int i = 10;  //程式碼 1i = 20;      //程式碼 2

場景 1:A 執行緒執行程式碼 1 和程式碼 2,然後 B 執行緒執行程式碼 1 和程式碼 2,CAS 成功。

場景 2:A 執行緒執行程式碼 1,此時 B 執行緒執行程式碼 1 和程式碼 2,A 執行緒執行程式碼 2,CAS 不成功,為什麼呢?

因為 A 執行緒執行程式碼 1 時候會舊值(i 的記憶體地址的值 10)儲存起來,執行程式碼 2 的時候先判斷 i 的最新值(可能被其他執行緒修改了)跟舊值比較,如果相等則把 i 賦值為 20,如果不是則 CAS 不成功。CAS 是一個原子性操作,要麼成功要麼失敗,CAS 操作用得比較多的是 sun.misc 包的 Unsafe 類,而 Java 併發包大量使用 Unsafe 類的 CAS 操作,比如:AtomicInteger 整數原子類(本質是自旋鎖 + CAS),CAS 不需加鎖,提高程式碼執行效率。也是一種樂觀鎖方式,我們通常認為在大多數場景下不會出現競爭資源的情況,如果 CAS 操作失敗,會不斷重試直到成功。

CAS 優點:資源競爭不大的場景系統開銷小。

CAS 缺點

如果 CAS 長時間操作失敗,即長時間自旋,會導致 CPU 開銷大,但是可以使用 CPU 提供的 pause 指令,這個 pause 指令可以讓自旋重試失敗時 CPU 先睡眠一小段時間後再繼續自旋重試 CAS 操作,jvm 支援 pause 指令,可以讓效能提升一些。存在 ABA 問題,即原來記憶體地址的值是 A,然後被改為了 B,再被改為 A 值,此時 CAS 操作時認為該值未被改動過,ABA 問題可以引入版本號來解決,每次改動都讓版本號 +1。Java 中處理 ABA 的一個方案是 AtomicStampedReference 類,它是使用一個 int 型別的欄位作為版本號,每次修改之前都先獲取版本號和當前執行緒持有的版本號比對,如果一致才進行修改操作,並把版本號 +1。無法保證程式碼塊的原子性,CAS 只能保證單個變數的原子性操作,如果要保證多個變數的原子性操作就要使用悲觀鎖了。AQS(AbstractQueuedSynchronizer)原理分析

字面意思是抽象的佇列同步器,AQS 是一個同步器框架,它制定了一套多執行緒場景下訪問共享資源的方案,Java 中很多同步類底層都是使用 AQS 實現,比如:ReentrantLock、CountDownLatch、ReentrantReadWriteLock,這些 java 同步類的內部會使用一個 Sync 內部類,而這個 Sync 繼承了 AbstractQueuedSynchronizer 類,這是一種模板方法模式,所以說這些同步類的底層是使用 AQS 實現。

AQS 內部維護了一個 volatile 修飾的 int state 屬性(共享資源)和一個先進先出的執行緒等待佇列(即多執行緒競爭共享資源時被阻塞的執行緒會進入這個佇列)。因為 state 是使用 volatile 修飾,所以在多執行緒之前可見,訪問 state 的方式有 3 種,getState()、setState()和 compareAndSetState()。

AQS 定義了 3 種資源共享方式:

獨佔鎖(exclusive),保證只有一條執行緒執行,比如 ReentrantLock、AtomicInteger。共享鎖(shared),允許多個執行緒同時執行,比如 CountDownLatch、Semaphore。同時實現獨佔和共享,比如 ReentrantReadWriteLock,允許多個執行緒同時執行讀操作,只允許一條執行緒執行寫操作。

ReentrantLock 和 CountDownLatch 都是自定義同步器,它們的內部類 Sync 都是繼承了 AbstractQueuedSynchronizer,獨佔鎖和共享鎖的區別在於各自重寫的獲取和釋放共享資源的方式不一樣,至於執行緒獲取資源失敗、喚醒出隊、中斷等操作 AQS 已經實現好了。

ReentrantLock

state 的初始值是 0,即沒有被鎖定,當 A 執行緒 tryAcquire() 時會獨佔鎖住 state,並且把 state+1,然後 B 執行緒(即其他執行緒)tryAcquire() 時就會失敗進入等待佇列,直到 A 執行緒 tryRelease() 釋放鎖把 state-1,此時也有可能出現重入鎖的情況,state-1 後的值不是 0 而是一個正整數,因為重入鎖也會 state+1,只有當 state=0 時,才代表其他執行緒可以 tryAcquire() 獲取鎖。

CountDownLatch

8 人賽跑場景,即開啟 8 個執行緒進行賽跑,state 的初始值設定為 8(必須與執行緒數一致),每個參賽者跑到終點(即執行緒執行完畢)則呼叫 countDown(),使用 CAS 操作把 state-1,直到 8 個參賽者都跑到終點了(即 state=0),此時呼叫 await() 判斷 state 是否為 0,如果是 0 則不阻塞繼續執行後面的程式碼。

tryAcquire()、tryRelease()、tryAcquireShared()、tryReleaseShared() 的詳細流程分析

tryAcquire() 詳細流程如下:

呼叫 tryAcquire() 嘗試獲取共享資源,如果成功則返回 true;如果不成功,則呼叫 addWaiter() 把此執行緒構造一個 Node 節點(標記為獨佔模式),並使用 CAS 操作把節點追加到等待佇列的尾部,然後該 Node 節點的執行緒進入自旋狀態;執行緒自旋時,判斷自旋節點的前驅節點是不是頭結點,並且已經釋放共享資源(即 state=0),自旋節點是否成功獲取共享資源(即 state=1),如果三個條件都成立則自旋節點設定為頭節點,如果不成立則把自旋節點的執行緒掛起,等待前驅節點喚醒。

tryRelease() 詳細流程如下:

呼叫 tryRelease() 釋放共享資源,即 state=0,然後喚醒沒有被中斷的後驅節點的執行緒;被喚醒的執行緒自旋,判斷自旋節點的前驅節點是不是頭結點,是否已經釋放共享資源(即 state=0),自旋節點是否成功獲取共享資源(即 state=1),如果三個條件都成立則自旋節點設定為頭節點,如果不成立則把自旋節點的執行緒掛起,等待被前驅節點喚醒。

tryAcquireShared() 詳細流程如下:

呼叫 tryAcquireShared() 嘗試獲取共享資源,如果 state>=0,則表示同步狀態(state)有剩餘還可以讓其他執行緒獲取共享資源,此時獲取成功返回;如果 state<0,則表示獲取共享資源失敗,把此執行緒構造一個 Node 節點(標記為共享模式),並使用 CAS 操作把節點追加到等待佇列的尾部,然後該 Node 節點的執行緒進入自旋狀態;執行緒自旋時,判斷自旋節點的前驅節點是不是頭結點,是否已經釋放共享資源(即 state=0),再呼叫 tryAcquireShared() 嘗試獲取共享資源,如果三個條件都成立,則表示自旋節點可執行,同時把自旋節點設定為頭節點,並且喚醒所有後繼節點的執行緒。如果不成立,掛起自旋的執行緒,等待被前驅節點喚醒。

tryReleaseShared() 詳細流程如下:

呼叫 tryReleaseShared() 釋放共享資源,即 state-1,然後遍歷整個佇列,喚醒所有沒有被中斷的後驅節點的執行緒;被喚醒的執行緒自旋,判斷自旋節點的前驅節點是不是頭結點,是否已經釋放共享資源(即 state=0),再呼叫 tryAcquireShared() 嘗試獲取共享資源,如果三個條件都成立,則表示自旋節點可執行,同時把自旋節點設定為頭節點,並且喚醒所有後繼節點的執行緒。如果不成立,掛起自旋的執行緒,等待被前驅節點喚醒。最後

覺得不錯的小夥伴記得轉發關注哦,後續會持續更新精選技術文章!

12
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 噓,彆著急!讓騰訊架構師告訴你為什麼要分庫分表