AQS學習(一)自旋鎖原理介紹(為什麼AQS底層使用自旋鎖佇列?)

小熊餐館發表於2021-08-13

1.什麼是自旋鎖?

  自旋鎖作為鎖的一種,和互斥鎖一樣也是為了在併發環境下保護共享資源的一種鎖機制。在任意時刻,只有一個執行單元能夠獲得鎖。

  互斥鎖通常利用作業系統提供的執行緒阻塞/喚醒機制實現,在爭用鎖失敗時令執行緒陷入阻塞態而讓出cpu,並在獲取到鎖時再將其喚醒。而自旋鎖則是通過加鎖程式中的無限迴圈,由當前嘗試加鎖的執行緒反覆輪訓當前鎖的狀態直到最終獲取到鎖。

互斥鎖與自旋鎖的優缺點

  互斥鎖的優點是當加鎖失敗時,執行緒會及時的讓出cpu,從而提高cpu的利用率,但缺點是如果短時間內如果涉及到大量執行緒的加鎖/解鎖,則頻繁的喚醒/阻塞會因為大量的執行緒上下文切換而降低系統的效能。因此互斥鎖適用於執行緒會在較長時間內持有鎖的場景。

      與互斥鎖相對的,自旋鎖由於一直處於持續不斷的輪訓中,因此可以非常迅速的感知到鎖狀態的變化,在兩個執行緒間能夠瞬間完成鎖的釋放與獲取。但如果需要爭用鎖的執行緒長時間都無法獲取到鎖,則會造成CPU長時間空轉,造成CPU資源極大的浪費。因此自旋鎖只適用於執行緒在加鎖成功後會在極短的時間內釋放鎖的場景(需要保護的臨界區非常小)。

      自旋鎖和互斥鎖起到了一個互補的作用,在不同的需求場景下發揮自己的作用。

2.自旋鎖的多種實現

  本篇部落格的重點是自旋鎖的工作原理,由於存在許多種擁有不同特性的自旋鎖,所以這裡只挑選出幾種具有代表性的自旋鎖:原始版本自旋鎖、票鎖TicketLock、CLH鎖和MCS鎖,介紹這幾種自選鎖的實現原理和各有的優缺點。

  本篇部落格中的自旋鎖是用java實現的。為了方便測試,先抽象並定義了一個通用的自旋鎖介面SpinLock。 

public interface SpinLock {

    /**
     * 加鎖
     * */
    void lock();

    /**
     * 解鎖
     * */
    void unlock();
}

2.1 原始自旋鎖

  原始版本的自旋鎖非常基礎,實現思路是所有需要加鎖的執行緒通過cas重試的方式去爭用鎖,如果cas設定lockOwner成功則代表加鎖成功;而解鎖的時候則將lockOwner設定為null即可,這樣後續需要爭用鎖的某個執行緒其lock中無限迴圈的cas操作就能成功獲取到鎖了。

 原始自旋鎖實現:

public class OriginalSpinLock implements SpinLock{
    /**
     * 標識當前自旋鎖的持有執行緒
     *
     * AtomicReference型別配合cas
     * */
    private final AtomicReference<Thread> lockOwner = new AtomicReference<>();

    @Override
    public void lock() {
        Thread currentThread = Thread.currentThread();

        // cas爭用鎖
        // 只有當加鎖時之前lockOwner為null,才代表加鎖成功,結束迴圈
        // 否則說明加鎖時已經有其它執行緒獲得了鎖,無限迴圈重試
        while (!lockOwner.compareAndSet(null, currentThread)) {
        }
    }

    @Override
    public void unlock() {
        Thread currentThread = Thread.currentThread();

        // cas釋放鎖
        // 只有之前加鎖成功的執行緒才能夠將其重新cas的設定為null
        lockOwner.compareAndSet(currentThread, null);
    }
} 

原始自旋鎖的優點:

  1.簡單:作為基礎版本的自旋鎖,無論是效能還是功能上都是最差的,其唯一的優點就是簡單了。

原始自旋鎖的缺點:

  1.非公平:相比其它型別的自旋鎖,原始自旋鎖的一大缺點是非公平。由於所有的執行緒都是監聽lockOwner這一引用,因此後加鎖的執行緒是很可能比在這之前已經在爭用鎖的執行緒先加鎖成功的,在大量執行緒參與加鎖的極端情況下會導致先加鎖的執行緒一直無法加鎖成功。

  2.過多的記憶體競爭:由於所有的執行緒都是監聽、訪問同一記憶體地址的資料,且AtomicReference中使用volatile關鍵字修飾value來保證執行緒間可見性。在多核cpu的架構下,作業系統和硬體底層會使用諸如鎖記憶體匯流排或者使用快取一致性協議同步等機制來實現不同執行緒間的記憶體可見性,這會在一定程度上影響到系統的記憶體訪問效能。

2.2 票鎖(TicketLock)

  為了解決原始自旋鎖非公平的缺點,在原始自旋鎖基礎上改進的票鎖TicketLock被發明瞭出來。

  票鎖在加鎖時當前執行緒會預先原子性的拿到一個逐步遞增且唯一的排隊服務號,只有當前票鎖的服務票號和自己拿到的排隊服務號一致時才認為加鎖成功。而在解鎖時則將當前票鎖的服務票號遞增,得以讓下一個加鎖的執行緒獲得鎖。

  由於服務票號是逐步遞增且唯一的,TicketLock中先來申請加鎖的執行緒會拿到更小更靠前的服務號,也能較之後申請加鎖的執行緒更早的獲得到鎖,保證了公平性。

票鎖實現:

public class TicketSpinLock implements SpinLock{

    /**
     * 排隊號發號器
     * */
    private AtomicInteger ticketNum = new AtomicInteger();

    /**
     * 當前服務號
     * */
    private AtomicInteger currentServerNum = new AtomicInteger();


    public void lock() {
        // 首先獲得一個唯一的排隊號
        int myTicketNum = ticketNum.getAndIncrement();

        // 當前服務號與自己持有的服務號不匹配
        // 一直無限輪訓,直到排隊號與自己的服務號一致(等待排隊排到自己)
        while (currentServerNum.get() != myTicketNum) {
        }
    }

    public void unlock() {
        // 釋放鎖時,代表當前服務已經結束
        // 當前服務號自增,使得拿到下一個服務號的執行緒能夠獲得鎖
        currentServerNum.incrementAndGet();
    }
}

票鎖的優點:

  1.公平:相比於原始自旋鎖,票鎖是一個先來先服務的公平鎖,避免了某些執行緒被其他執行緒搶先而長時間無法獲取鎖的問題。

票鎖的缺點:

      1.過多的記憶體競爭:和原始自旋鎖一樣,票鎖中每一個執行緒都會不斷的訪問當前服務票號(currentServerNum)這一volatile關鍵字修飾的變數值,在多核CPU架構下效能會受到一定的影響。

2.3 CLH鎖

      CLH鎖是由Craig, Landin, and Hagersten三位電腦科學家共同發明的,這也是CLH鎖名字的由來(取名字首字母)。

  CLH鎖被發明出來的主要原因是為了解決多核cpu體系中全部加鎖執行緒都訪問同一記憶體地址而出現過多記憶體競爭的問題。CLH鎖和票鎖一樣是先來先服務的公平鎖,但CLH鎖引入了執行緒節點的概念,需要加鎖的執行緒不斷的從隊尾加入佇列,構造出了一個邏輯上的單向連結串列佇列;獲取鎖的順序也是從佇列頭部開始,早加入佇列的執行緒便能更早的獲得到CLH鎖,實現先來先服務的公平性。

CLH鎖結構圖:

      CLH鎖中加鎖的執行緒不再是統一的監聽同一個標識鎖狀態的記憶體地址,而是隻監聽佇列中當前執行緒節點其前驅執行緒節點的鎖狀態。如此一來,便分散了不同執行緒加鎖時所要訪問的記憶體變數地址,相比起前面介紹的原始自旋鎖和票鎖減少了大量的記憶體訪問競爭,減少了底層為了實現執行緒間記憶體資料可見性同步時的效能開銷。

      加鎖時,先cas的入隊獲取前驅節點後,便不斷的迴圈監聽前驅節點鎖的狀態,當發現前驅節點釋放了鎖時,當前節點便獲得了鎖。

      而解鎖時則很簡單,將當前執行緒自己的鎖狀態更改為已釋放即可。標識為已釋放時,存在的後繼加鎖節點便能感知到這一變化,從而獲得鎖。

CLH鎖實現:

/**
 * 原始版CLH鎖(無顯式prev前驅節點引用,無法支援取消加鎖等場景)
 */
public class CLHLockV1 implements SpinLock{
    private static class CLHNode {
        /**
         * 獲取到鎖的執行緒其後繼爭用鎖的節點會持續不斷的查詢isLocked的值
         * 使用volatile修飾,使得釋放鎖修改isLocked時不會出現執行緒間變數不可見的問題
         * */
        private volatile boolean isLocked;
    }

    private final AtomicReference<CLHNode> tailNode;
    private final ThreadLocal<CLHNode> curNode;

    public CLHLockV1() {
        // 初始化時尾結點指向一個空的CLH節點
        tailNode = new AtomicReference<>(new CLHNode());
        // 設定threadLocal的初始化方法
        curNode = ThreadLocal.withInitial(CLHNode::new);
    }

    @Override
    public void lock() {
        CLHNode currNode = curNode.get();
        currNode.isLocked = true;

        // cas的設定當前節點為tail尾節點,並且獲取到設定前的老tail節點
        // 老的tail節點是當前加鎖節點的前驅節點(隱式前驅節點),當前節點通過監聽其isLocked狀態來判斷其是否已經解鎖
        CLHNode preNode = tailNode.getAndSet(currNode);
        while (preNode.isLocked) {
            // 無限迴圈,等待獲得鎖
        }

        // 迴圈結束,說明其前驅已經釋放了鎖,當前執行緒加鎖成功
    }

    @Override
    public void unlock() {
        CLHNode node = curNode.get();
        // 清除當前threadLocal中的節點,避免再次Lock加鎖時獲取到之前的節點
        curNode.remove();
        node.isLocked = false;
    }
}

CLH鎖的優點:

  1.公平:FIFO的執行緒佇列保證了先加鎖的執行緒能更早一步的獲得鎖,不會被後加鎖的執行緒搶先,保證了公平性。

  2.分散了記憶體競爭:由於每個需要加鎖的執行緒監聽的是其前驅節點的鎖狀態,所以不同執行緒監聽的是不同的記憶體資料,避免了所有執行緒都監聽同一記憶體資料的效能問題。

CLH鎖的缺點:

      1.不支援取消加鎖的場景:當前示例中的CLH鎖是基礎版的,沒有顯式的連結前驅節點,無法支援超時等取消鎖的場景。

2.4 MCS鎖

  MCS鎖也是得名於其發明者的名字,John Mellor-Crummey和Michael Scott。

  MCS鎖的實現思路和CLH鎖類似,也是通過構建一個單向連結串列來分攤記憶體競爭的並實現先來先服務的公平性。

MCS鎖結構圖:

MCS鎖實現:

public class MCSLock implements SpinLock{

    private static class MCSNode {
        /**
         * 獲取到鎖的執行緒其後繼爭用鎖的節點會持續不斷的查詢isLocked的值
         * 使用volatile修飾,使得釋放鎖修改isLocked時不會出現執行緒間變數不可見的問題
         * */
        private volatile boolean isLocked;

        private MCSNode next;
    }

    private final AtomicReference<MCSNode> tailNode;
    private final ThreadLocal<MCSNode> curNode;

    public MCSLock() {
        // MCS鎖的tailNode初始化時為空,代表初始化時沒有任何執行緒持有鎖
        tailNode = new AtomicReference<>();
        // 設定threadLocal的初始化方法
        curNode = ThreadLocal.withInitial(MCSNode::new);
    }

    @Override
    public void lock() {
        MCSNode currNode = curNode.get();
        currNode.isLocked = true;

        MCSNode preNode = tailNode.getAndSet(currNode);
        if(preNode == null){
            // 當前執行緒加鎖之前並不存在tail節點,則代表當前執行緒為最新的節點,直接認為是加鎖成功
            currNode.isLocked = false;
        }else{
            // 之前的節點存在,令前驅節點next指向當前節點,以便後續前驅節點釋放鎖時能夠找到currNode
            // 前驅節點釋放鎖時,會主動的更新currNode.isLocked(令currNode.isLocked=false)
            preNode.next = currNode;

            while (currNode.isLocked) {
                // 自旋等待當前節點自己的isLocked變為false
            }
        }
    }

    @Override
    public void unlock() {
        MCSNode currNode = curNode.get();
        if (currNode == null || currNode.isLocked) {
            // 前置防禦性校驗,如果當前執行緒節點為空或者當前執行緒自身沒有成功獲得鎖,則直接返回,加鎖失敗
            return;
        }

        if(currNode.next == null){
            // 當前節點的next為空,說明其是MCS的最後一個節點
            // 以cas的形式將tailNode設定為null(防止此時有執行緒併發加鎖 => lock方法中的tailNode.getAndSet())
            boolean casSuccess = tailNode.compareAndSet(currNode,null);
            if(casSuccess){
                // 如果cas設定tailNode成功為null成功,則釋放鎖結束
                return;
            }else{
                // 如果cas設定失敗,說明此時又有了新的執行緒節點入隊了
                while (currNode.next == null) {
                    // 自旋等待,併發lock的執行緒執行(preNode.next = currNode),設定currNode的next引用
                }
            }
        }

        // 如果currNode.next存在,按照約定則釋放鎖時需要將其next的isLocked修改,令next節點執行緒結束自旋從而獲得鎖
        currNode.next.isLocked = false;
        // 方便GC,斷開next引用
        currNode.next = null;
    }
}

MCS鎖的優點:

  1.公平:FIFO的執行緒佇列保證了先加鎖的執行緒能更早一步的獲得鎖,不會被後加鎖的執行緒搶先,保證了公平性。

  2.分散了記憶體競爭:由於每個需要加鎖的執行緒監聽的是其前驅節點的鎖狀態,所以不同執行緒監聽的是不同的記憶體資料,避免了所有執行緒都監聽同一記憶體資料的效能問題。

  3.NUMA架構下效能更好NUMA架構下MCS鎖的效能略優於CLH鎖。

MCS鎖的缺點:

      1..不支援取消加鎖的場景:和基礎版的CLH鎖一樣,無法支援超時等取消鎖的場景。

3.為什麼CLH鎖在NUMA的CPU架構下效能會略低於MCS鎖?

  從上述CLH鎖和MCS鎖的實現中可以看到,MCS鎖的連結串列佇列方向和CLH鎖是相反的,CLH鎖是一個從尾節點出發通過prev關聯前驅節點的佇列,後繼節點通過無限迴圈監聽並感知其前驅節點的鎖狀態變化;而MCS鎖是一個從頭節點出發通過next關聯後繼節點的單向佇列,在前驅節點釋放鎖時通過修改後繼節點的鎖狀態來通知後繼節點。

  兩種鎖的實現方式看起來大同小異,但細小的區別卻使得MCS鎖在NUMA架構下的效能要高於CLH鎖。

3.1 什麼是NUMA架構?

  NUMA架構是多核CPU體系架構的一種,與之相對的則是SMP架構。

  SMP(Sysmmetric Multi-Processor System,對稱多處理器系統)架構顧名思義,多個cpu核心通過統一的方式共享訪問同一個集中式的儲存器,每個cpu並無主從之分被分配同樣大小的時間片平均的訪問儲存器。

  SMP架構比起NUMA架構簡單,早期的多核CPU都是採用這種架構。但SMP架構受限於儲存器匯流排的頻寬,核心數過多容易導致部分核心無法得到足夠的訪問時間片而陷入飢餓,因此SMP架構其所能支援的CPU核心數受到了很大的限制。  

SMP結構示意圖:

  為了解決SMP架構下儲存器頻寬有限的問題,電腦科學家提出了一種新的CPU體系架構即NUMA架構(Non-Uniform Memory Access,非一致性儲存器訪問與SMP架構不同,NUMA架構下的儲存器是分散式的,其將cpu核心和儲存器平均分割為了多個NUMA節點(NUMA Node),不同節點之間通過QPI匯流排等機制進行互聯。

  NUMA架構下任意的cpu核心依然可以訪問全量的儲存器,cpu核心如果訪問的是位於同一節點內的儲存器會很快,但訪問其它NUMA節點內的儲存器則由於需要通過QPI匯流排訪問而會有一定的延遲,這也是其被稱為非一致儲存器訪問的原因。

  NUMA架構下cpu核心訪問本地節點記憶體快,訪問遠端節點記憶體慢。理想情況下,每個節點如果都只訪問本地節點的記憶體,那麼理論上其資料吞吐量將會是SMP架構的N倍(N為節點數)。

NUMA結構示意圖: 

3.2 CLH鎖在NUMA架構下低於MCS鎖的原因

  在簡單瞭解了NUMA架構後,下面開始說明CLH鎖在NUMA架構下低於MCS鎖的原因。

  由於NUMA架構下訪問本地節點記憶體和遠端節點記憶體效能存在差異,作業系統在為執行緒分配記憶體時需要儘可能的讓執行緒訪問的記憶體與執行執行緒的cpu分配到同一個NUMA節點中,一個簡單的策略便是將對應執行緒所申請的記憶體分配到對應執行緒cpu所在的同一NUMA節點中。(實際的NUMA結構下記憶體分配與平衡機制很複雜,因為記憶體分配後如果執行緒發生上下文切換後可能就在其它節點的cpu核心上了,這裡舉的例子極大的簡化了複雜度)

  觀察CLH鎖和MCS鎖在NUMA架構下的行為:CLH鎖和MCS鎖為執行緒節點分配的記憶體通常都會分配到與對應執行緒執行cpu核心繫結的NUMA節點的儲存器中,而不同執行緒對應的cpu則可能位於不同的NUMA節點中。CLH鎖會無限輪訓前驅節點的isLocked狀態,這一操作在前驅節點執行緒與當前執行緒不位於同一NUMA節點時,會不斷的進行遠端節點訪問,效能較低;而MCS鎖中,當前執行緒則是無限輪訓自己執行緒節點的isLocked,這種情況下都是本地NUMA節點記憶體的訪問。

  當前驅節點執行緒與當前節點執行緒不在同一NUMA節點內時,CLH鎖在lock時會進行N次遠端節點訪問,在unLock時進行一次本地節點訪問;而MCS鎖則在lock時會進行N次本地節點訪問,並在unLock時進行一次遠端節點訪問。

  綜上所述,由於NUMA節點下本地節點訪問效能是優於遠端節點訪問的,因此MCS鎖的效能在NUMA架構下會略優於CLH鎖。

4.為什麼AQS框架底層使用CLH佇列結構作為基礎?

  這個問題可以被分解為兩個更細節的問題,即為什麼AQS底層使用自旋鎖佇列作為基礎以及為什麼在自旋鎖佇列中選擇了CLH鎖佇列而不是MCS鎖佇列作為基礎

4.1 為什麼AQS底層使用自旋鎖佇列作為基礎?

  AQS是jdk的juc併發工具包下提供的抽象同步器框架,可作為可重入鎖、訊號量等各型別同步器的實現基礎。

  由於AQS中需要讓大量併發爭用鎖的執行緒頻繁的被阻塞和喚醒,出於效能的考慮,為避免過多的執行緒上下文切換,AQS本身沒有再利用作業系統底層提供的執行緒阻塞/喚醒機制通過互斥鎖來保證同步佇列的併發安全,而是使用基於CAS的樂觀重試機制來構造一個無鎖,併發安全的同步佇列。

  AQS論文原文:These days, there is little controversy that the most appropriate choices for synchronization queues are non-blocking data structures that do not themselves need to be constructed using lower-level locks.(如今,構造同步佇列最合適的選擇是使用自身構造不依賴底層鎖的無鎖資料結構,這在業界是幾乎沒有爭議的)

    從AQS的作者Doug Lea的論文中可以看到,AQS作為一個用於控制執行緒併發的底層框架,為避免互斥鎖同步機制下過多的執行緒上下文切換而影響效能,所以才使用不需要阻塞執行緒的自旋鎖佇列作為基礎來實現執行緒安全的。

4.2 為什麼AQS使用CLH鎖佇列而不是MCS鎖佇列作為基礎呢?

  AQS作為一個通用的同步器框架,是需要支援超時、中斷等取消加鎖的,而前面提到基礎版的CLH鎖和MCS鎖都存在一個缺陷,即無法很好的支援超時、中斷等取消加鎖的場景。

  引入了顯式前驅節點引用的CLH鎖比起MCS鎖可以更加簡單的實現超時、中斷等加鎖過程中臨時退出加鎖的場景。而由於AQS中的執行緒在徵用鎖失敗時不會佔用CPU一直自旋等待,而是被設定為阻塞態讓出CPU(LockSupport.park),因此MCS鎖在NUMA架構下效能略高的優點也就不是那麼重要了。

  AQS論文原文:Historically, CLH locks have been used only in spinlocks. However, they appeared more amenable than MCS for use in the synchronizer framework because they are more easily adapted to handle cancellation and timeouts, so were chosen as a basis. The resulting design is far enough removed from the original CLH structure to require explanation.一直以來,CLH鎖僅被用於自旋鎖。然而,在這個框架中,CLH鎖顯然比MCS鎖更合適。因為CLH鎖可以更容易地去實現“取消”和“超時”功能,因此我們選擇了CLH鎖作為實現的基礎。但是最終的設計已經與原來的CLH鎖結構有較大的出入)

引入顯示前驅節點的CLH鎖實現:

public class CLHLockV2 implements SpinLock{

    private static class CLHNode {
        private volatile CLHNode prev;
        private volatile boolean isLocked;

        public CLHNode() {
        }

        public CLHNode(CLHNode prev, boolean isLocked) {
            this.prev = prev;
            this.isLocked = isLocked;
        }
    }

    private static final CLHNode DUMMY_NODE = new CLHNode(null,false);

    private final CLHNode head;
    private final AtomicReference<CLHNode> tail;
    private final ThreadLocal<CLHNode> curNode;

    public CLHLockV2() {
        head = DUMMY_NODE;
        tail = new AtomicReference<>(DUMMY_NODE);
        curNode = ThreadLocal.withInitial(CLHNode::new);
    }

    @Override
    public void lock() {
        CLHNode currentNode = curNode.get();
        currentNode.isLocked = true;

        // cas的設定為當前tail為新的tail節點
        currentNode.prev = tail.getAndSet(currentNode);

        while(true){
            while(currentNode.prev.isLocked){
            }

            // 內層while迴圈結束,說明前驅節點已經釋放了鎖
            CLHNode prevNode = currentNode.prev;
            if(prevNode == head){
                // 如果前驅節點為head(Dummy節點)
                return;
            }else{
                currentNode.prev = prevNode.prev;
            }
        }
    }

    @Override
    public void unlock() {
        CLHNode currentNode = curNode.get();
        curNode.remove();
        currentNode.isLocked = false;
    }
}

  熟悉AQS實現的讀者可以看到,引入了顯式前驅節點引用的CLH鎖改良版和AQS的同步佇列實現已經有了幾分相似。不過由於AQS中加鎖失敗的節點不是通過自旋來感知能否獲取到鎖,而是依賴其同步佇列的前驅節點來喚醒它,因此AQS和用於自旋鎖的CLH鎖在最終實現上存在一定差異。(The wait queue is a variant of a "CLH" (Craig, Landin, and Hagersten) lock queue. CLH locks are normally used for spinlocks)。

5.總結

  作為AQS框架學習的第一篇部落格,之所以決定從自旋鎖的工作原理開始展開,是因為AQS框架的底層是CLH鎖佇列的一個變種。如果能理解CLH鎖佇列的工作模式,可以為AQS的學習提供很大的幫助。由於AQS需要能夠為互斥鎖、共享鎖和條件變數等多種不同型別的同步器提供基礎支援,程式碼量較多;且其基於樂觀鎖重試的特點,使得程式碼中存在著不少處理臨界區無鎖併發的晦澀邏輯,原始碼讀起來比較吃力。因此在學習的過程中轉換思維,嘗試著站在AQS設計者的角度來理解其工作原理,參考著AQS的實現思路自己動手寫一個簡易版的AQS,在這個過程中將自己的實現和AQS的原始碼進行比較,結合網上關於AQS原理解析的部落格,反覆琢磨和體會作者Doug Lea實現的巧妙之處。 

  自己實現的AQS會按照順序通過支援互斥鎖、支援共享鎖、支援取消加鎖(中斷、超時退出)和支援條件變數這幾個功能模組為基礎逐步完成,後續會以部落格的形式分享出來。

  希望這篇部落格能幫助到對自旋鎖、AQS工作原理感興趣的人,如有錯誤,還請多多指教。

參考書籍:

  《多處理器程式設計的藝術》

參考部落格:

  https://www.cnblogs.com/dennyzhangdd/p/7218510.html AQS框架論文翻譯

  https://www.cnblogs.com/stevenczp/p/7136416.html 多種自旋鎖實現

  https://felord.blog.csdn.net/article/details/108313803 CLH鎖的實現

  https://javazhiyin.blog.csdn.net/article/details/108332477 CLH鎖詳解

  https://www.cnblogs.com/xingzheanan/p/10547387.html NUMA架構介紹 

相關文章