CLH lock 原理及JAVA實現


 

 --喜歡記得關注我喲【shoshana】--

 

 

前記

JUC中的Lock中最核心的類AQS,其中AQS使用到了CLH隊列的變種,故來研究一下CLH隊列的原理及JAVA實現

 

一. CLH背景知識

SMP(Symmetric Multi-Processor)。即對稱多處理器結構,指server中多個CPU對稱工作,每一個CPU訪問內存地址所需時間同樣。其主要特征是共享,包括對CPU,內存,I/O等進行共享。SMP的長處是可以保證內存一致性。缺點是這些共享的資源非常可能成為性能瓶頸。隨着CPU數量的添加,每一個CPU都要訪問同樣的內存資源,可能導致內存訪問沖突,可能會導致CPU資源的浪費。經常使用的PC機就屬於這樣的。
NUMA(Non-Uniform Memory Access)非一致存儲訪問,將CPU分為CPU模塊,每一個CPU模塊由多個CPU組成,而且具有獨立的本地內存、I/O槽口等,模塊之間能夠通過互聯模塊相互訪問,訪問本地內存的速度將遠遠高於訪問遠地內存(系統內其他節點的內存)的速度,這也是非一致存儲訪問NUMA的由來。NUMA長處是能夠較好地解決原來SMP系統的擴展問題,缺點是因為訪問遠地內存的延時遠遠超過本地內存,因此當CPU數量添加時。系統性能無法線性添加。

CLH 鎖的名字也與他們的發明人的名字相關:Craig,Landin and Hagersten。

CLH Lock摘要

CLH lock is Craig, Landin, and Hagersten (CLH) locks, CLH lock is a spin lock, can ensure no hunger, provide fairness first come first service.
The CLH lock is a scalable, high performance, fairness and spin lock based on the list, the application thread spin only on a local variable, it constantly polling the precursor state, if it is found that the pre release lock end spin.

CLH鎖是自旋鎖的一種,對它的研究是因為AQS源代碼中使用了CLH鎖的一個變種,為了更好的理解AQS中使用鎖的思想,所以決定先好好理解CLH鎖

二. CLH原理

CLH也是一種基於單向鏈表(隱式創建)的高性能、公平的自旋鎖,申請加鎖的線程只需要在其前驅節點的本地變量上自旋,從而極大地減少了不必要的處理器緩存同步的次數,降低了總線和內存的開銷。

三. Java代碼實現

類圖

 

 
         
public interface Lock {
    void lock();

    void unlock();
}

public class QNode {
    volatile boolean locked;
}


import java.util.concurrent.atomic.AtomicReference;

public class CLHLock implements Lock {
    // 尾巴,是所有線程共有的一個。所有線程進來后,把自己設置為tail
    private final AtomicReference<QNode> tail;
    // 前驅節點,每個線程獨有一個。
    private final ThreadLocal<QNode> myPred;
    // 當前節點,表示自己,每個線程獨有一個。
    private final ThreadLocal<QNode> myNode;

    public CLHLock() {
        this.tail = new AtomicReference<QNode>(new QNode());
        this.myNode = new ThreadLocal<QNode>() {
            protected QNode initialValue() {
                return new QNode();
            }
        };
        this.myPred = new ThreadLocal<QNode>();
    }

    @Override
    public void lock() {
        // 獲取當前線程的代表節點
        QNode node = myNode.get();
        // 將自己的狀態設置為true表示獲取鎖。
        node.locked = true;
        // 將自己放在隊列的尾巴,並且返回以前的值。第一次進將獲取構造函數中的那個new QNode
        QNode pred = tail.getAndSet(node);
        // 把舊的節點放入前驅節點。
        myPred.set(pred);
        // 判斷前驅節點的狀態,然后走掉。
        while (pred.locked) {
        }
    }

    @Override
    public void unlock() {
        // unlock. 獲取自己的node。把自己的locked設置為false。
        QNode node = myNode.get();
        node.locked = false;
        myNode.set(myPred.get());
    }
}
 
         

簡單的看一下CLH的算法定義

the list, the application thread spin only on a local variable, it constantly polling the precursor state, if it is found that the pre release lock end spin.

基於list,線程僅在一個局部變量上自旋,它不斷輪詢前一個節點狀態,如果發現前一個節點釋放鎖結束.

所以在java中使用了ThreadLocal作為具體實現,AtomicReference為了消除多個線程並發對tail引用Node的影響,核心方法lock()中分為3個步驟去實現

  1. 初始狀態 tail指向一個node(head)節點

    private final AtomicReference<Node> tail = new AtomicReference<Node>(new Node());
  2. thread加入等待隊列: tail指向新的Node,同時Prev指向tail之前指向的節點,在java代碼中使用了getAndSet即CAS操作使用

    Node pred = this.tail.getAndSet(node); this.prev.set(pred);
  3. 尋找當前線程對應的node的前驅node然后開始自旋前驅node的status判斷是否可以獲取lock

    while (pred.locked);

同理unlock()方法,獲取當前線程的node,設置lock status,將當前node指向前驅node(這樣操作tail指向的就是前驅node等同於出隊操作).至此CLH Lock的過程就結束了

測試CLHLock

public class CLHLockDemo2 {

    public static void main(String[] args) {
        final Kfc kfc = new Kfc();
        for (int i = 0; i < 10; i++) {
            new Thread("eat" + i) {
                public void run() {
                    kfc.eat();
                }
            }.start();
        }

    }
}

class Kfc {
    private final Lock lock = new CLHLock();
    private int i = 0;

    public void eat() {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + ": " + --i);
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void cook() {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + ": " + ++i);
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

 運行結果

eat1: -1
eat0: -2
eat3: -3
eat4: -4
eat7: -5
eat2: -6
eat5: -7
eat6: -8
eat8: -9
eat9: -10

 

四. CLH優缺點

CLH隊列鎖的優點是空間復雜度低(如果有n個線程,L個鎖,每個線程每次只獲取一個鎖,那么需要的存儲空間是O(L+n),n個線程有n個myNode,L個鎖有L個tail),CLH的一種變體被應用在了JAVA並發框架中。唯一的缺點是在NUMA系統結構下性能很差,在這種系統結構下,每個線程有自己的內存,如果前趨結點的內存位置比較遠,自旋判斷前趨結點的locked域,性能將大打折扣,但是在SMP系統結構下該法還是非常有效的一種解決NUMA系統結構的思路是MCS隊列鎖

五. 了解與CLH對應的MCS自旋鎖

MCS 自旋鎖

MCS 的名稱來自其發明人的名字:John Mellor-Crummey和Michael Scott。
MCS 的實現是基於鏈表的,每個申請鎖的線程都是鏈表上的一個節點,這些線程會一直輪詢自己的本地變量,來知道它自己是否獲得了鎖。已經獲得了鎖的線程在釋放鎖的時候,負責通知其它線程,這樣 CPU 之間緩存的同步操作就減少了很多,僅在線程通知另外一個線程的時候發生,降低了系統總線和內存的開銷。實現如下所示:

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; public class MCSLock { public static class MCSNode { volatile MCSNode next; volatile boolean isWaiting = true; // 默認是在等待鎖 } volatile MCSNode queue;// 指向最后一個申請鎖的MCSNode private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater .newUpdater(MCSLock.class, MCSNode.class, "queue"); public void lock(MCSNode currentThread) { MCSNode predecessor = UPDATER.getAndSet(this, currentThread);// step 1 if (predecessor != null) { predecessor.next = currentThread;// step 2 while (currentThread.isWaiting) {// step 3 } } else { // 只有一個線程在使用鎖,沒有前驅來通知它,所以得自己標記自己已獲得鎖 currentThread.isWaiting = false; } } public void unlock(MCSNode currentThread) { if (currentThread.isWaiting) {// 鎖擁有者進行釋放鎖才有意義 return; } if (currentThread.next == null) {// 檢查是否有人排在自己后面 if (UPDATER.compareAndSet(this, currentThread, null)) {// step 4 // compareAndSet返回true表示確實沒有人排在自己后面 return; } else { // 突然有人排在自己后面了,可能還不知道是誰,下面是等待后續者 // 這里之所以要忙等是因為:step 1執行完后,step 2可能還沒執行完 while (currentThread.next == null) { // step 5 } } } currentThread.next.isWaiting = false; currentThread.next = null;// for GC } } 

MCS 的能夠保證較高的效率,降低不必要的性能消耗,並且它是公平的自旋鎖。

CLH 鎖與 MCS 鎖的原理大致相同,都是各個線程輪詢各自關注的變量,來避免多個線程對同一個變量的輪詢,從而從 CPU 緩存一致性的角度上減少了系統的消耗。
CLH 鎖與 MCS 鎖最大的不同是,MCS 輪詢的是當前隊列節點的變量,而 CLH 輪詢的是當前節點的前驅節點的變量,來判斷前一個線程是否釋放了鎖。
 

小結

CLH Lock是一種比較簡單的自旋鎖算法之一,因為鎖的CAS操作涉及到了硬件的鎖定(鎖總線或者是鎖內存)所以性能和CPU架構也密不可分,該興趣的同學可以繼續深入研究包括MCS鎖等。CLH Lock是獨占式鎖的一種,並且是不可重入的鎖,這篇文章是對AQS鎖源代碼分析的預熱篇

 

參考內容:

https://segmentfault.com/a/1190000007094429

https://blog.csdn.net/faicm/article/details/80501465

https://blog.csdn.net/aesop_wubo/article/details/7533186

https://www.jianshu.com/p/0f6d3530d46b

https://blog.csdn.net/jjavaboy/article/details/78603477


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM