深入剖析AQS和CAS,看了都說好

前言

不知不覺寫文章已經快半年了,本來之前寫文章只是爲了自己總結知識,不知不覺中關注的朋友越來越多了。

現在寫文章不單單只是爲了考慮自己能看懂,還要考慮各位讀者大大是否能看懂,考慮輸出文章的質量。

現在的每一次寫作就好像在搞一次藝術品,細細雕琢,進行每一次的加工。文章的邏輯性易懂性,還有文章的排版的美觀度,都要細細斟酌。

寫在前面先來一碗雞湯:世界上並沒有什麼救世主,假如有那便是你自己;世界上也沒有什麼奇蹟,假如有那只是努力的另一個名字罷了。

想想自己畢業差不多一年來走過來的路,看看現在的自己,一切都值得,往後還會不斷的努力,看到越來越強的自己。

話不多說下面就直接上乾貨了,今天來深入的瞭解CASAQS,文章採用層次式、圖文並茂的方式一層一層的進行剖析,讓各位讀者大大能夠深入理解。

AQS簡介

AQS(AbstractQueuedSynchronizer)抽象隊列同步器,簡單的說AQS就是一個抽象類,抽象類就是AbstractQueuedSynchronizer,沒有實現任何的接口,僅僅定義了同步狀態(state)的獲取和釋放的方法

它提供了一個FIFO隊列,多線程競爭資源的時候,沒有競爭到的線程就會進入隊列中進行等待,並且定義了一套多線程訪問共享資源的同步框架

在AQS中的鎖類型有兩種:分別是Exclusive(獨佔鎖)Share(共享鎖)

獨佔鎖就是每次都只有一個線程運行,例如ReentrantLock。關於ReentrantLock之前寫過一片詳細的源碼文章,喜歡的可以看一看[]。

共享鎖就是同時可以多個線程運行,如Semaphore、CountDownLatch、ReentrantReadWriteLock

AQS源碼分析

在AQS的源碼可以看到對於state共享變量,使用volatile關鍵字進行修飾,從而保證了可見性,若是對於volatile關鍵字不熟悉的可以參考這一篇[]。


從上面的源碼中可以看出,對於state的修改操作提供了setStatecompareAndSetState那麼爲什麼要提供這兩個對state的修改呢?

因爲compareAndSetState方法通常使用在獲取到鎖之前,當前線程不是鎖持有者,對於state的修改可能存在線程安全問題,所以需要保證對state修改的原子性操作

setState方法通常用於當前正持有鎖的線程對state共享變量進行修改,因爲不存在競爭,是線程安全的,所以沒必要使用CAS操作。

分析了AQS的源碼的實現,接下來我們看看AQS的實現的原理。這裏AQS的實現源碼和理論都會比較簡單,因爲還沒有涉及到具體的實現類。

AQS實現原理

上面說到AQS中維護了一個FIFO隊列,並且該隊列式一個雙向鏈表,鏈表中的每一個節點爲Node節點Node類是AbstractQueuedSynchronizer中的一個內部類

讓我們來看看AQS中Node內部類的源碼,這樣有助於我們能夠對AQS的內部的實現更加的清晰:

 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節點擁有的屬性進行維護,在Node內部類中最重要的基本構成就是這幾個:

    volatile Node prev;
    volatile Node next;
    volatile Thread thread;

根據源碼的分析和線程的競爭共享資源的原理,關於AQS的實現原理,我這裏畫了一張圖:

在FIFO隊列中,頭節點佔有鎖,也就是頭節點纔是鎖的持有者,尾指針指向隊列的最後一個等待線程節點,除了頭節點和尾節點,節點之間都有前驅指針後繼指針

在AQS中維護了一個共享變量state,標識當前的資源是否被線程持有,多線程競爭的時候,會去判斷state是否爲0,嘗試的去把state修改爲1

分析了AQS的源碼的實現和原理實現,但是AQS裏面具體是沒有做同步的具體實現,如果要什麼瞭解AQS的具體的實現原理,要需要看AQS的具體實現類,這邊就以ReentrantLock爲例。

ReentrantLock實現原理

如果多線程在競爭共享資源時,競爭失敗的線程就會添加入FIFO隊列的尾部

ReentrantLock的的具體實現中,這邊以在ReentrantLock的非公平鎖的實現爲例,因爲公平鎖的實現,之前已經寫過一篇文章分析過了。

我們來看看新添加節點的源碼寫的實現邏輯:

當競爭鎖資源的線程失敗後直接進入acquire(1)方法,來來看看acquire(1)的具體實現:

從源碼中可以看出,acquire(1)的實現主要有這三步的邏輯:

  1. tryAcquire(arg):嘗試再次獲取鎖。
  2. addWaiter(Node.EXCLUSIVE):若是獲取鎖失敗,就會將當前線程組裝成一個Node節點,進行如對操作。
  3. acquireQueued(addWaiter(Node.EXCLUSIVE), arg)):acquireQueued方法以addWaiter返回的頭節點作爲參數,內部實現進行鎖自旋,以及判斷是否應該執行線程掛起。

下面我們再來看看tryAcquire(arg)的源碼,從上面的看一看出arg的值爲1,具體的實現源碼如下:



從源碼的分析中可以看出,tryAcquire(arg)的實現也就是判斷state的值是否已經被釋放,若釋放則當前線程就會CAS操作將state設置爲1,若是沒有釋放,就會判斷是否可以進行鎖的重入

分析完tryAcquire(arg)的實現,來看看addWaiter,入隊操作的實現源碼如下:

從上面的源碼分析,可以看出對於新加入的線程添加到雙向鏈表中使用尾插法,具體的實現原理圖如下所示。

從上圖分析,當線程假如隊列中,主要進行這幾步操作新加入的節點prev指針指向原來的tail節點,原來的tail節點的next指針指向新加入的節點,這個也就是常見的雙向列表尾插法的操作。

最後把tail指向新加入的節點,如此一來就完成了新加入節點的入隊操作,接下來我們接着分析源碼。

當然這裏的前提是隊列中不爲空,若是爲空的話,不會走上面的邏輯,而是走enq(node),進行初始化節點,我們來看看enq(node)操作,源碼如下:

執行完上面的入對操作後,接着執行acquireQueued方法,來看看它的具體實現源碼:

從上上面的源碼中可以看出,涉及到頭節點head的出隊操作,並且將當前線程的node節點晉升爲head節點

因爲只有頭節點纔是鎖的持有者,所以對於head節點的出隊操作,head的指向會隨時改變,我這裏畫了一張原理圖如下所示:

具體實現如上圖所示,會把原來的頭節點進行出隊操作,也就是把原來的頭節點next指針指向null,原來第二節點的prev指針指向null

最後把head指針指向第二節點,當然thread2同時還會修改共享狀態變量state的值,如此一來就完成了鎖的釋放。

當釋放完鎖之後,就會執行shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()判斷當前的線程是否應該被掛起,我們來看看它的源碼實現:

shouldParkAfterFailedAcquire中的實現,當前驅節點的狀態量waitStatusSIGNAL的時候,就會掛起。

通過上面的分析對於AQS的實現基本有比較清晰的認識,主要是對實現類ReentrantLock的實現原理進行深入的分析,並且是基於非公平鎖獨佔鎖的實現。

在AQS的底層維護了一個FIFO隊列,多線程競爭共享資源的時候,失敗的線程會被添加入隊列中非公平鎖實現中,新加入的線程節點會自旋嘗試的獲取鎖。

分析完AQS我們來分析CAS,那麼什麼是CAS呢?

CAS簡介

在分析ReentrantLock的具體實現的源碼中,可以看出所有涉及設置共享變量的操作,都會指向CAS操作,保證原子性操作。

CAS(compare and swap)原語理解就是比較並交換的意思,CAS是一種樂觀鎖的實現

在CAS的算法實現中有三個值:更新的變量舊的值新值。在修改共享資源時候,會與原值進行比較,若是等於原值,就修改爲新值。

於是在這裏的算法實現下,即使不加鎖,也能保證數據的可見性,即使的發現數據是否被更改,若是數據已經被更新則寫操作失敗。

但是CAS也會引發ABA的問題,什麼是ABA問題呢? 不慌請聽我詳細道來

ABA問題

ABA問題就是假如有兩個線程,同一時間讀取一個共享變量state=1,此時兩個線程都已經將state的副本賦值到自己的工作內存中。

當線程一對state修改state=state+1,並且寫入到主存中,然後線程一又對state=state-1寫入到主存,此時主存的state是變化了兩次,只不過又變回了原來的值。

那麼此時線程二修改state的時候就會修改成功,這就是ABA問題。對於ABA問題的解決方案就是加版本號(version),每次進行比較的時候,也會比較版本號。

因爲版本版是隻增不減,比如以時間作爲版本號,每一時刻的時間都不一樣,這樣就能避免ABA的問題。

CAS性能分析

相對於synchronized的阻塞算法的實現,CAS採用的是樂觀鎖的非阻塞算法的實現,一般CPU在進行線程的上下文切換的時間比執行CPU的指令集的時間長,所以CAS操作在性能上也有了很大的提升。

但是所有的算法都是沒有最完美的,在執行CAS的操作中,沒有更新成功的就會自旋,這樣也會消耗CPU的資源,對於CPU來說是不友好的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章