CLH lock queue的原理解釋及Java實現(xiàn)

@[toc]

背景

相信大部分人在看AQS的時候都能看到注釋上有這么一段話:

The wait queue is a variant of a "CLH" (Craig, Landin, and Hagersten) lock queue.

為了更好的理解AQS中使用鎖的思想,所以決定先好好理解CLH鎖。
在網上能查到很多關于CLH的博客,但不如我想象中的那么全面,于是自己來整理一篇清晰點的。

原理解釋

論文地址
CLH的作者:Craig, Landin, and Hagersten毁嗦。

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.

我們能看到它是一個自旋鎖,能確保無饑餓性回铛,提供先來先服務的公平性狗准。同時它也是一種基于鏈表的可擴展、高性能茵肃、公平的自旋鎖腔长,申請線程只在本地變量上自旋,它不斷輪詢前驅的狀態(tài)验残,如果發(fā)現(xiàn)前驅釋放了鎖就結束自旋饼酿。

這個算法很妙的點在于,在一個CAS操作幫助下胚膊,所有等待獲取鎖的線程之下的節(jié)點輕松且正確地構建成了全局隊列。等待中的線程正如隊列中的節(jié)點依次獲取鎖想鹰。

接下來就說一下這個算法的Java實現(xiàn)紊婉。

Java代碼實現(xiàn)

CLHLock類結構

這里面貼出的代碼是主要流程代碼,詳細代碼在GitHub中辑舷。

定義QNode

CLH隊列中的節(jié)點QNode中含有一個locked字段喻犁,該字段若為true表示該線程需要獲取鎖,且不釋放鎖何缓,為false表示線程釋放了鎖肢础。

public class QNode {
   
    volatile boolean locked;
}

定義Lock接口

public interface Lock {
 
    void lock();
 
    void unlock();
}

定義CLHLock

節(jié)點之間是通過隱形的鏈表相連,之所以叫隱形的鏈表是因為這些節(jié)點之間沒有明顯的next指針碌廓,而是通過myPred所指向的節(jié)點的變化情況來影響myNode的行為传轰。CLHLock上還有一個尾指針,始終指向隊列的最后一個節(jié)點谷婆。

public class CLHLock implements Lock {
    // 尾巴慨蛙,是所有線程共有的一個。所有線程進來后纪挎,把自己設置為tail
    private final AtomicReference<QNode> tail;
    // 前驅節(jié)點期贫,每個線程獨有一個。
    private final ThreadLocal<QNode> myPred;
    // 當前節(jié)點异袄,表示自己通砍,每個線程獨有一個。
    private final ThreadLocal<QNode> myNode;

    public CLHLock() {
        this.tail = new AtomicReference<>(new QNode());
        this.myNode = ThreadLocal.withInitial(QNode::new);
        this.myPred = new ThreadLocal<>();
    }

    @Override
    public void lock() {
        // 獲取當前線程的代表節(jié)點
        QNode node = myNode.get();
        // 將自己的狀態(tài)設置為true表示獲取鎖烤蜕。
        node.locked = true;
        // 將自己放在隊列的尾巴封孙,并且返回以前的值迹冤。第一次進將獲取構造函數(shù)中的那個new QNode
        QNode pred = tail.getAndSet(node);
        // 把舊的節(jié)點放入前驅節(jié)點。
        myPred.set(pred);
        // 在等待前驅節(jié)點的locked域變?yōu)閒alse敛瓷,這是一個自旋等待的過程
        while (pred.locked) {
        }
        // 打印myNode叁巨、myPred的信息
        peekNodeInfo();
    }

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

使用場景

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

    public void takeout() {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + ": 拿了第" + ++i + "份外賣");
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

運行代碼

public static void main(String[] args) {
    final KFC kfc = new KFC();
    Executor executor = Executors.newFixedThreadPool(5);
    for (int i = 1; i <= 35; i++) {
        executor.execute(kfc::takeout);
    }
}

代碼輸出

為什么輸出這么多日志后面有解釋锋勺。

thread-1 acquire lock success. myNode(QNode_2_locked:true), myPred(QNode_1_locked:false)
thread-1: 拿了第1份外賣
thread-2 acquire lock success. myNode(QNode_3_locked:true), myPred(QNode_2_locked:false)
thread-2: 拿了第2份外賣
thread-3 acquire lock success. myNode(QNode_4_locked:true), myPred(QNode_3_locked:false)
thread-3: 拿了第3份外賣
thread-4 acquire lock success. myNode(QNode_5_locked:true), myPred(QNode_4_locked:false)
thread-4: 拿了第4份外賣
thread-5 acquire lock success. myNode(QNode_6_locked:true), myPred(QNode_5_locked:false)
thread-5: 拿了第5份外賣
----------------------------------------------------------------------------------------
thread-1 acquire lock success. myNode(QNode_1_locked:true), myPred(QNode_6_locked:false)
thread-1: 拿了第6份外賣
thread-2 acquire lock success. myNode(QNode_2_locked:true), myPred(QNode_1_locked:false)
thread-2: 拿了第7份外賣
thread-3 acquire lock success. myNode(QNode_3_locked:true), myPred(QNode_2_locked:false)
thread-3: 拿了第8份外賣
thread-4 acquire lock success. myNode(QNode_4_locked:true), myPred(QNode_3_locked:false)
thread-4: 拿了第9份外賣
thread-5 acquire lock success. myNode(QNode_5_locked:true), myPred(QNode_4_locked:false)
thread-5: 拿了第10份外賣
----------------------------------------------------------------------------------------
thread-1 acquire lock success. myNode(QNode_6_locked:true), myPred(QNode_5_locked:false)
thread-1: 拿了第11份外賣
thread-2 acquire lock success. myNode(QNode_1_locked:true), myPred(QNode_6_locked:false)
thread-2: 拿了第12份外賣
thread-3 acquire lock success. myNode(QNode_2_locked:true), myPred(QNode_1_locked:false)
thread-3: 拿了第13份外賣
thread-4 acquire lock success. myNode(QNode_3_locked:true), myPred(QNode_2_locked:false)
thread-4: 拿了第14份外賣
thread-5 acquire lock success. myNode(QNode_4_locked:true), myPred(QNode_3_locked:false)
thread-5: 拿了第15份外賣
----------------------------------------------------------------------------------------
thread-1 acquire lock success. myNode(QNode_5_locked:true), myPred(QNode_4_locked:false)
thread-1: 拿了第16份外賣
thread-2 acquire lock success. myNode(QNode_6_locked:true), myPred(QNode_5_locked:false)
thread-2: 拿了第17份外賣
thread-3 acquire lock success. myNode(QNode_1_locked:true), myPred(QNode_6_locked:false)
thread-3: 拿了第18份外賣
thread-4 acquire lock success. myNode(QNode_2_locked:true), myPred(QNode_1_locked:false)
thread-4: 拿了第19份外賣
thread-5 acquire lock success. myNode(QNode_3_locked:true), myPred(QNode_2_locked:false)
thread-5: 拿了第20份外賣
----------------------------------------------------------------------------------------
thread-1 acquire lock success. myNode(QNode_4_locked:true), myPred(QNode_3_locked:false)
thread-1: 拿了第21份外賣
thread-2 acquire lock success. myNode(QNode_5_locked:true), myPred(QNode_4_locked:false)
thread-2: 拿了第22份外賣
thread-3 acquire lock success. myNode(QNode_6_locked:true), myPred(QNode_5_locked:false)
thread-3: 拿了第23份外賣
thread-4 acquire lock success. myNode(QNode_1_locked:true), myPred(QNode_6_locked:false)
thread-4: 拿了第24份外賣
thread-5 acquire lock success. myNode(QNode_2_locked:true), myPred(QNode_1_locked:false)
thread-5: 拿了第25份外賣
----------------------------------------------------------------------------------------
thread-1 acquire lock success. myNode(QNode_3_locked:true), myPred(QNode_2_locked:false)
thread-1: 拿了第26份外賣
thread-2 acquire lock success. myNode(QNode_4_locked:true), myPred(QNode_3_locked:false)
thread-2: 拿了第27份外賣
thread-3 acquire lock success. myNode(QNode_5_locked:true), myPred(QNode_4_locked:false)
thread-3: 拿了第28份外賣
thread-4 acquire lock success. myNode(QNode_6_locked:true), myPred(QNode_5_locked:false)
thread-4: 拿了第29份外賣
thread-5 acquire lock success. myNode(QNode_1_locked:true), myPred(QNode_6_locked:false)
thread-5: 拿了第30份外賣
----------------------------------------------------------------------------------------
thread-1 acquire lock success. myNode(QNode_2_locked:true), myPred(QNode_1_locked:false)
thread-1: 拿了第31份外賣
thread-2 acquire lock success. myNode(QNode_3_locked:true), myPred(QNode_2_locked:false)
thread-2: 拿了第32份外賣
thread-3 acquire lock success. myNode(QNode_4_locked:true), myPred(QNode_3_locked:false)
thread-3: 拿了第33份外賣
thread-4 acquire lock success. myNode(QNode_5_locked:true), myPred(QNode_4_locked:false)
thread-4: 拿了第34份外賣
thread-5 acquire lock success. myNode(QNode_6_locked:true), myPred(QNode_5_locked:false)
thread-5: 拿了第35份外賣

代碼解釋

CLHLock的加鎖、釋放鎖過程

  • 當一個線程需要獲取鎖時狡蝶,會創(chuàng)建一個新的QNode庶橱,將其中的locked設置為true表示需要獲取鎖,然后線程對tail域調用getAndSet方法贪惹,使自己成為隊列的尾部苏章,同時返回舊的尾部節(jié)點作為其前驅引用myPred,然后該線程就在前驅節(jié)點的locked字段上自旋,直到前驅節(jié)點釋放鎖奏瞬。

  • 當一個線程需要釋放鎖時枫绅,將當前節(jié)點的locked域設置為false,同時回收前驅節(jié)點硼端。如下圖所示并淋,線程A需要獲取鎖,其myNode域為true珍昨,此時tail指向線程A的節(jié)點县耽,然后線程B也加入到線程A后面,tail指向線程B的節(jié)點镣典。然后線程A和B都在它的myPred域上旋轉兔毙,一旦它的myPred節(jié)點的locked字段變?yōu)閒alse,它就可以獲取到鎖兄春。明顯線程A的myPred locked值為false澎剥,此時線程A獲取到了鎖。
    <img src="https://img-blog.csdnimg.cn/20200719100502129.png" width="60%" alt="CLHLock示意圖"/>

第一個使用CLHLock的線程自動獲取到鎖

初始狀態(tài)的tail的值是一個新的QNode神郊,locked的值是默認的false肴裙。后面新加入的QNode在獲取鎖的時候都把locked置為true后放入尾節(jié)點并成為前驅節(jié)點。

為什么使用ThreadLocal保存myNode和myPred涌乳?

因為每個線程使用lock方法的時候蜻懦,將QNode綁定到當前線程上,等unlock操作的時候還可以獲取到本線程之前調用lock方法里創(chuàng)建的QNode對象夕晓。

為什么tail要用AtomicReference修飾宛乃?

因為我們在把尾節(jié)點更新成當前節(jié)點并返回舊的尾節(jié)點作為前驅節(jié)點的時候,我們希望這個操作是原子性的,AtomicReference的getAndSet()方法正好能滿足我們的需求征炼。

unlock中的set操作怎么理解析既?

也就是這一段代碼:

myNode.set(myPred.get());

它有以下幾點影響:

  • 將當前node指向前驅node,lock方法中再也拿不到當前node的引用了谆奥。這樣操作等于把當前node從鏈表頭部刪除(并不是被JVM回收眼坏,第二個線程的myPred還引用它)
  • 當前線程若要在unlock之后再次拿鎖需重新排隊(每個線程自己都維護了兩個QNode,一個在釋放鎖的時候把當前node置為前驅node酸些,另一個在lock方法的時候重新獲取尾node作為前驅node)
  • 如果所有的任務都是由固定數(shù)量的線程池執(zhí)行的話宰译,你會看到所有的QNode的使用會形成一個環(huán)形鏈表(實際不是),在打印日志中可看到魄懂,日志“拿了第31份”和日志“拿了第1份”的myNode和myPred一樣沿侈。

為什么要有myPred,不用行不行市栗?

也就是代碼改成這樣:

public void lock() {
    QNode node = myNode.get();
    node.locked = true;

    // spin on pre-node
    QNode pred = tail.getAndSet(node);
    while (pred.locked) {
    }
}

public void unlock() {
    QNode node = myNode.get();
    node.locked = false;
}

答案肯定是不行啦缀拭。
假設有兩個線程:T1 & T2,T1持有鎖填帽,T2等待T1釋放鎖蛛淋。
這時候T1.node.locked為true套鹅,T2.node.locked也為true枕荞,tail變量指向T2.node瀑粥,卻T2正在pred.locked自旋音比。這里的pred也是T1.node。

現(xiàn)在T1開始釋放鎖(設置T1.node.locked為false)并且在T2搶占到鎖之前再次獲取鎖苫拍,此時T1.node.locked再次變成true,但是此時的尾節(jié)點是T2.node,所以T1只好等待T2釋放鎖绘迁。而T2也在等待T1釋放鎖,死鎖發(fā)生了卒密。

再結合上面myNode.set(myPred.get())代碼的解釋缀台,myPred變量提供了兩點好處:

  • 防止死鎖發(fā)生,釋放鎖的時候也就釋放了當前節(jié)點的引用哮奇。
  • 等待隊列中節(jié)點具有順序性(看日志打犹鸥)可保證鎖競爭公平,每個等待鎖的線程都持有前驅節(jié)點的引用(getAndSet返回)鼎俘,n個線程最后有n+1的節(jié)點(有一個是初始tail的node)哲身,所有的節(jié)點按照順序循環(huán)使用。借助于myPred在釋放鎖后若要再次拿鎖需排隊且排在最后贸伐。

CLH優(yōu)缺點

CLH隊列鎖的優(yōu)點是空間復雜度低(如果有n個線程勘天,L個鎖,每個線程每次只獲取一個鎖,那么需要的存儲空間是O(L+n)脯丝,n個線程有n個QNode商膊,L個鎖有L個tail)。

CLH的一種變體被應用在了JAVA并發(fā)框架中宠进。唯一的缺點是在NUMA系統(tǒng)結構下性能很差晕拆,在這種系統(tǒng)結構下,每個線程有自己的內存材蹬,如果前趨結點的內存位置比較遠实幕,自旋判斷前趨結點的locked域,性能將大打折扣赚导,但是在SMP系統(tǒng)結構下該法還是非常有效的茬缩。一種解決NUMA系統(tǒng)結構的思路是MCS隊列鎖。

NUMA與SMP

SMP(Symmetric Multi-Processor)吼旧,即對稱多處理器結構凰锡,指服務器中多個CPU對稱工作,每個CPU訪問內存地址所需時間相同圈暗。其主要特征是共享掂为,包含對CPU,內存员串,I/O等進行共享勇哗。SMP的優(yōu)點是能夠保證內存一致性,缺點是這些共享的資源很可能成為性能瓶頸寸齐,隨著CPU數(shù)量的增加欲诺,每個CPU都要訪問相同的內存資源,可能導致內存訪問沖突渺鹦,可能會導致CPU資源的浪費扰法。常用的PC機就屬于這種。

NUMA(Non-Uniform Memory Access)非一致存儲訪問毅厚,將CPU分為CPU模塊塞颁,每個CPU模塊由多個CPU組成,并且具有獨立的本地內存吸耿、I/O槽口等祠锣,模塊之間可以通過互聯(lián)模塊相互訪問,訪問本地內存的速度將遠遠高于訪問遠地內存(系統(tǒng)內其它節(jié)點的內存)的速度咽安,這也是非一致存儲訪問NUMA的由來伴网。NUMA優(yōu)點是可以較好地解決原來SMP系統(tǒng)的擴展問題,缺點是由于訪問遠地內存的延時遠遠超過本地內存妆棒,因此當CPU數(shù)量增加時是偷,系統(tǒng)性能無法線性增加拳氢。

最后

我的疑惑點都在上文敘述了,如果還有不清晰的地方蛋铆,希望可以在評論區(qū)指出馋评,謝謝??

參考

A Hierarchical CLH Queue Lock
JAVA并發(fā)編程學習筆記之CLH隊列鎖
CLH lock 原理及JAVA實現(xiàn)
Why CLH Lock need prev-Node in java

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市刺啦,隨后出現(xiàn)的幾起案子留特,更是在濱河造成了極大的恐慌,老刑警劉巖玛瘸,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蜕青,死亡現(xiàn)場離奇詭異,居然都是意外死亡糊渊,警方通過查閱死者的電腦和手機右核,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來渺绒,“玉大人贺喝,你說我怎么就攤上這事∽诩妫” “怎么了躏鱼?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長殷绍。 經常有香客問我染苛,道長,這世上最難降的妖魔是什么主到? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任茶行,我火速辦了婚禮,結果婚禮上登钥,老公的妹妹穿的比我還像新娘拢军。我一直安慰自己,他們只是感情好怔鳖,可當我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著固蛾,像睡著了一般结执。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上艾凯,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天献幔,我揣著相機與錄音,去河邊找鬼趾诗。 笑死蜡感,一個胖子當著我的面吹牛蹬蚁,可吹牛的內容都是我干的。 我是一名探鬼主播郑兴,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼犀斋,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了情连?” 一聲冷哼從身側響起叽粹,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎却舀,沒想到半個月后虫几,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡挽拔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年辆脸,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片螃诅。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡啡氢,死狀恐怖,靈堂內的尸體忽然破棺而出州刽,到底是詐尸還是另有隱情空执,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布穗椅,位于F島的核電站辨绊,受9級特大地震影響,放射性物質發(fā)生泄漏匹表。R本人自食惡果不足惜门坷,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望袍镀。 院中可真熱鬧默蚌,春花似錦、人聲如沸苇羡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽设江。三九已至锦茁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間叉存,已是汗流浹背码俩。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留歼捏,地道東北人稿存。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓笨篷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親瓣履。 傳聞我的和親對象是個殘疾皇子率翅,可洞房花燭夜當晚...
    茶點故事閱讀 45,060評論 2 355