☕【Java技術指南】「併發原理專題」AQS的技術體系之CLH、MCS鎖的原理及實現

李浩宇Alex發表於2021-08-21

背景

SMP(Symmetric Multi-Processor)

對稱多處理器結構,它是相對非對稱多處理技術而言的、應用十分廣泛的並行技術

  • 在這種架構中,一臺計算機由多個CPU組成,並共享記憶體和其他資源,所有的CPU都可以平等地訪問記憶體、I/O和外部中斷。
  • 雖然同時使用多個CPU,但是從管理的角度來看,它們的表現就像一臺單機一樣。
  • 作業系統將任務佇列對稱地分佈於多個CPU之上,從而極大地提高了整個系統的資料處理能力。
  • 但是隨著CPU數量的增加,每個CPU都要訪問相同的記憶體資源,共享資源可能會成為系統瓶頸,導致CPU資源浪費

NUMA(Non-Uniform Memory Access)

非一致儲存訪問,將CPU分為CPU模組,每個CPU模組由多個CPU組成,並且具有獨立的本地記憶體、I/O槽口等,模組之間可以通過互聯模組相互訪問

  • 訪問本地記憶體(本CPU模組的記憶體)的速度將遠遠高於訪問遠端記憶體(其他CPU模組的記憶體)的速度,這也是非一致儲存訪問的由來。

  • NUMA較好地解決SMP的擴充套件問題,當CPU數量增加時,因為訪問遠地記憶體的延時遠遠超過本地記憶體,系統效能無法線性增加。


CLH鎖

CLH是一種基於單向連結串列的高效能、公平的自旋鎖。申請加鎖的執行緒通過前驅節點的變數進行自旋。在前置節點解鎖後,當前節點會結束自旋,並進行加鎖。

  • 在SMP架構下,CLH更具有優勢。
  • 在NUMA架構下,如果當前節點與前驅節點不在同一CPU模組下,跨CPU模組會帶來額外的系統開銷,而MCS鎖更適用於NUMA架構。

加鎖邏輯

  1. 獲取當前執行緒的鎖節點,如果為空,則進行初始化;

  2. 同步方法獲取連結串列的尾節點,並將當前節點置為尾節點,此時原來的尾節點為當前節點的前置節點。

  3. 如果尾節點為空,表示當前節點是第一個節點,直接加鎖成功。

  4. 如果尾節點不為空,則基於前置節點的鎖值(locked==true)進行自旋,直到前置節點的鎖值變為false。

解鎖邏輯

  1. 獲取當前執行緒對應的鎖節點,如果節點為空或者鎖值為false,則無需解鎖,直接返回;

  2. 同步方法為尾節點賦空值,賦值不成功表示當前節點不是尾節點,則需要將當前節點的locked=false解鎖節點。如果當前節點是尾節點,則無需為該節點設定。


public class CLHLock {
    private final AtomicReference<Node> tail;
    private final ThreadLocal<Node> myNode;
    private final ThreadLocal<Node> myPred;
 
    public CLHLock() {
        tail = new AtomicReference<>(new Node());
        myNode = ThreadLocal.withInitial(() -> new Node());
        myPred = ThreadLocal.withInitial(() -> null);
    }
 
    public void lock(){
        Node node = myNode.get();
        node.locked = true;
        Node pred = tail.getAndSet(node);
        myPred.set(pred);
        while (pred.locked){}
    }
 
    public void unLock(){
        Node node = myNode.get();
        node.locked=false;
        myNode.set(myPred.get());
    }
 
 
    static class Node {
        volatile boolean locked = false;
    }
 
}

MCS鎖

MSC與CLH最大的不同並不是連結串列是顯示還是隱式,而是執行緒自旋的規則不同:CLH是在前趨結點的locked域上自旋等待,而MCS是在自己的結點的locked域上自旋等待。正因為如此,它解決了CLH在NUMA系統架構中獲取locked域狀態記憶體過遠的問題

MCS鎖具體實現規則:

  • a. 佇列初始化時沒有結點,tail=null

  • b. 執行緒A想要獲取鎖,將自己置於隊尾,由於它是第一個結點,它的locked域為false

  • c. 執行緒B和C相繼加入佇列,a->next=b,b->next=c,B和C沒有獲取鎖,處於等待狀態,所以locked域為true,尾指標指向執行緒C對應的結點

  • d. 執行緒A釋放鎖後,順著它的next指標找到了執行緒B,並把B的locked域設定為false,這一動作會觸發執行緒B獲取鎖。

public class MCSLock {
 
    private final AtomicReference<Node> tail;
 
    private final ThreadLocal<Node> myNode;
 
    public MCSLock() {
        tail = new AtomicReference<>();
        myNode = ThreadLocal.withInitial(() -> new Node());
    }
 
    public void lock() {
 
        Node node = myNode.get();
        Node pred = tail.getAndSet(node);
        if (pred != null) {
            node.locked = true;
            pred.next = node;
            while (node.locked) {
            }
        }
 
    }
 
    public void unLock() {
        Node node = myNode.get();
        if (node.next == null) {
            if (tail.compareAndSet(node, null)) {
                return;
            }
 
            while (node.next == null) {
            }
        }
        node.next.locked = false;
        node.next = null;
    }
 
    class Node {
        volatile boolean locked = false;
        Node next = null;
    }
 
    public static void main(String[] args) {
 
        MCSLock lock = new MCSLock();
 
        Runnable task = new Runnable() {
            private int a;
 
            @Override
            public void run() {
                lock.lock();
                for (int i = 0; i < 10; i++) {
                    a++;
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(a);
                lock.unLock();
            }
        };
 
        new Thread(task).start();
        new Thread(task).start();
        new Thread(task).start();
        new Thread(task).start();
    }
}

相關文章