面試官:CAS和AQS底層原理了解?我:一篇文章堵住你的嘴

寫在前面

用XMind畫了一張導圖記錄Java併發編程的學習筆記和一些面試解析(源文件對部分節點有詳細備註和參考資料,歡迎關注個人公衆號:阿風的架構筆記後臺發送【併發】拿下載連接,已經完善更新): java

CAS(Compare And Swap)原理分析

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

int i = 10;  //代碼 1
i = 20;      //代碼 2
複製代碼

場景 1:A 線程執行代碼 1 和代碼 2,而後 B 線程執行代碼 1 和代碼 2,CAS 成功。編程

場景 2:A 線程執行代碼 1,此時 B 線程執行代碼 1 和代碼 2,A 線程執行代碼 2,CAS 不成功,爲何呢?markdown

由於 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 實現。框架

面試官:CAS和AQS底層原理了解?我:一篇文章堵住你的嘴

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

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() 詳細流程以下:

  1. 調用 tryAcquire() 嘗試獲取共享資源,若是成功則返回 true;
  2. 若是不成功,則調用 addWaiter() 把此線程構造一個 Node 節點(標記爲獨佔模式),並使用 CAS 操做把節點追加到等待隊列的尾部,而後該 Node 節點的線程進入自旋狀態;
  3. 線程自旋時,判斷自旋節點的前驅節點是否是頭結點,而且已經釋放共享資源(即 state=0),自旋節點是否成功獲取共享資源(即 state=1),若是三個條件都成立則自旋節點設置爲頭節點,若是不成立則把自旋節點的線程掛起,等待前驅節點喚醒。

面試官:CAS和AQS底層原理了解?我:一篇文章堵住你的嘴

tryRelease() 詳細流程以下:

  1. 調用 tryRelease() 釋放共享資源,即 state=0,而後喚醒沒有被中斷的後驅節點的線程;
  2. 被喚醒的線程自旋,判斷自旋節點的前驅節點是否是頭結點,是否已經釋放共享資源(即 state=0),自旋節點是否成功獲取共享資源(即 state=1),若是三個條件都成立則自旋節點設置爲頭節點,若是不成立則把自旋節點的線程掛起,等待被前驅節點喚醒。

tryAcquireShared() 詳細流程以下:

  1. 調用 tryAcquireShared() 嘗試獲取共享資源,若是 state>=0,則表示同步狀態(state)有剩餘還可讓其餘線程獲取共享資源,此時獲取成功返回;
  2. 若是 state<0,則表示獲取共享資源失敗,把此線程構造一個 Node 節點(標記爲共享模式),並使用 CAS 操做把節點追加到等待隊列的尾部,而後該 Node 節點的線程進入自旋狀態;
  3. 線程自旋時,判斷自旋節點的前驅節點是否是頭結點,是否已經釋放共享資源(即 state=0),再調用 tryAcquireShared() 嘗試獲取共享資源,若是三個條件都成立,則表示自旋節點可執行,同時把自旋節點設置爲頭節點,而且喚醒全部後繼節點的線程。
  4. 若是不成立,掛起自旋的線程,等待被前驅節點喚醒。

tryReleaseShared() 詳細流程以下:

  1. 調用 tryReleaseShared() 釋放共享資源,即 state-1,而後遍歷整個隊列,喚醒全部沒有被中斷的後驅節點的線程;
  2. 被喚醒的線程自旋,判斷自旋節點的前驅節點是否是頭結點,是否已經釋放共享資源(即 state=0),再調用 tryAcquireShared() 嘗試獲取共享資源,若是三個條件都成立,則表示自旋節點可執行,同時把自旋節點設置爲頭節點,而且喚醒全部後繼節點的線程。
  3. 若是不成立,掛起自旋的線程,等待被前驅節點喚醒。

看完三件事❤️

若是你以爲這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:

  1. 點贊,轉發,有大家的 『點贊和評論』,纔是我創造的動力。
  2. 關注公衆號 『 阿風的架構筆記 』,不按期分享原創知識。
  3. 同時能夠期待後續文章ing🚀
  4. 關注後回覆【666】掃碼便可獲取架構進階學習資料包

相關文章
相關標籤/搜索