自旋鎖學(xué)習(xí)記錄


參考地址

普通自旋鎖

利用AtomicReference.compareAndSet 確定對(duì)象的原子性,并通過while不斷循環(huán)阻塞其他線程。
當(dāng)上個(gè)線程unLock后,阻塞線程跳出while。

public class SpinningLock {
    /**
     * 持有鎖的線程 為空標(biāo)識(shí)沒有線程持有
     */
    private AtomicReference<Thread> ref = new AtomicReference<>();

    /**
     * 鎖
     */
    public void Lock(){
        // 獲取當(dāng)前線程
        Thread currentThread = Thread.currentThread();
        // ref.compareAndSet
        // 1. 當(dāng)ref =  null時(shí)  compareAndSet 把當(dāng)前線程賦值到ref 并返回true
        // 2. 當(dāng)ref != null時(shí)  compareAndSet 返回false
        while (!ref.compareAndSet(null, currentThread)){
            // 通過循環(huán)不斷的自旋判斷鎖是否被其他線程持有 hlod資源
        }
    }

    /**
     * 解鎖
     */
    public void unLock(){
        Thread currentThread = Thread.currentThread();
        ref.get();
        ref.compareAndSet(currentThread, null);
    }
}
優(yōu)點(diǎn):
  1. 無需上下文切換,速率快
缺點(diǎn):
  1. conpareAndSet是其核心,底層通過各系統(tǒng)cpu指令實(shí)現(xiàn)(依賴硬件)。
  2. 無法保證等待線程按FIFO順序獲得鎖(非公平)

自旋鎖變種(TicketLock-解決普通自旋鎖 公平性問題)

類似排號(hào)流程:

lock()

用戶A和B去醫(yī)院排號(hào),A到了1號(hào),B取到了2號(hào) => myNum.set(ticketNum.getAndIncrement());。
醫(yī)生按照票號(hào)順序叫號(hào)對(duì)A一頓服務(wù),B就老老實(shí)實(shí)坐板凳等著 => while (serviceNum.get() != myNum.get())

unlock()

當(dāng)醫(yī)生服務(wù)完后,看了眼A的票號(hào),下個(gè)應(yīng)該是2號(hào)治療了 => serviceNum.compareAndSet(myNum.get(), myNum.get() + 1);
然后將A請(qǐng)出去 => myNum.remove(); 讓還在while等著的B進(jìn)來。

public class TicketLock implements Lock {
    // 服務(wù)號(hào) 線程完成作業(yè) +1
    private AtomicInteger serviceNum = new AtomicInteger(0);
    // 取票號(hào) 線程進(jìn)入時(shí)取號(hào)
    private AtomicInteger ticketNum = new AtomicInteger(0);
    // 當(dāng)前線程持有號(hào)
    private final ThreadLocal<Integer> myNum = new ThreadLocal<>();
    @Override
    public void lock() {
        // 當(dāng)前線程取號(hào)
        myNum.set(ticketNum.getAndIncrement());
        // 當(dāng)服務(wù)號(hào) != 線程所取到的號(hào) 死循環(huán)阻塞 監(jiān)聽到serviceNum = myNum時(shí)退出循環(huán)
        while (serviceNum.get() != myNum.get()) {
        }
    }

    @Override
    public void unlock() {
        serviceNum.compareAndSet(myNum.get(), myNum.get() + 1);
        myNum.remove();
    }
}

該變種雖然解決了公平性問題,但是在多處理系統(tǒng)上需要對(duì)serviceNum進(jìn)行讀寫同步,增大了內(nèi)存和總線的流量,降低了系統(tǒng)整體性能。

自旋鎖變種(CLHLock)

CLHLock發(fā)明人是:Craig,Landin and Hagersten 所以才以CLH開頭。這是種基于鏈表,可擴(kuò)展和高性能的自旋鎖。
該設(shè)計(jì)的思想主要是將線程有序的抽象成一個(gè)個(gè)Node對(duì)象,利用對(duì)象的線程共享locked屬性,判斷是否存在上個(gè)節(jié)點(diǎn)持有鎖,以此阻塞或通過。每次獲取鎖時(shí)將當(dāng)前node放入尾部鏈表,將上個(gè)node放入前區(qū)鏈表;解鎖時(shí)獲取當(dāng)前node,置為false,讓后續(xù)線程通過,再將currNode置為preNode,因?yàn)槌跏蓟瘯r(shí)是個(gè)初始對(duì)象,相當(dāng)于平移,這樣就將當(dāng)前node移出節(jié)點(diǎn)

public class CLHLock implements Lock {
    // 指向尾部節(jié)點(diǎn)
    private final AtomicReference<QNode> tail;
    // 指向前驅(qū)節(jié)點(diǎn)
    private final ThreadLocal<QNode> preNode;
    // 當(dāng)前節(jié)點(diǎn)
    private final ThreadLocal<QNode> myNode;

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

    @Override
    public void lock() {
        // 獲取一個(gè)QNode
        QNode qnode = myNode.get();
        // 設(shè)置自己的狀態(tài)為locked=true表示需要獲取鎖
        qnode.locked = true;
        // 鏈表的尾部設(shè)置為本線程的qNode,并將之前的尾部設(shè)置為當(dāng)前線程的preNode
        QNode pre = tail.getAndSet(qnode);
        // 把舊的節(jié)點(diǎn)放入前驅(qū)節(jié)點(diǎn)。
        preNode.set(pre);
        // 當(dāng)前線程在前驅(qū)節(jié)點(diǎn)的locked字段上旋轉(zhuǎn),直到前驅(qū)節(jié)點(diǎn)釋放鎖資源
        while (pre.locked) {
        }

    }

    @Override
    public void unlock() {
        // 獲取當(dāng)前節(jié)點(diǎn)
        QNode qnode = myNode.get();
        // 釋放鎖操作時(shí)將自己的locked設(shè)置為false,可以使得自己的后繼節(jié)點(diǎn)可以結(jié)束自旋
        qnode.locked = false;
        // 回收自己這個(gè)節(jié)點(diǎn),從虛擬隊(duì)列中刪除
        // 將當(dāng)前節(jié)點(diǎn)引用置為自己的preNode,那么下一個(gè)節(jié)點(diǎn)的preNode就變?yōu)榱水?dāng)前節(jié)點(diǎn)的preNode,這樣就將當(dāng)前節(jié)點(diǎn)移出了隊(duì)列
        myNode.set(preNode.get());
    }

    private class QNode {
        // true表示該線程需要獲取鎖,且不釋放鎖,為false表示線程釋放了鎖,且不需要鎖 volatile 修飾其它線程可見
        private volatile boolean locked = false;
    }
優(yōu)點(diǎn):

空間復(fù)雜度低(如果有n個(gè)線程,L個(gè)鎖,每個(gè)線程每次只獲取一個(gè)鎖,那么需要的存儲(chǔ)空間是O(L+n),n個(gè)線程有n個(gè)myNode,L個(gè)鎖有L個(gè)tail),CLH的一種變體被應(yīng)用在了JAVA并發(fā)框架中

缺點(diǎn):

在NUMA系統(tǒng)結(jié)構(gòu)下性能很差(在這種系統(tǒng)結(jié)構(gòu)下,每個(gè)線程有自己的內(nèi)存,如果前趨結(jié)點(diǎn)的內(nèi)存位置比較遠(yuǎn),自旋判斷前趨結(jié)點(diǎn)的locked域,性能將大打折扣)

自旋鎖變種(MCSLock)

MCS 來自于其發(fā)明人名字的首字母: John Mellor-Crummey和Michael Scott。是一種基于鏈表的可擴(kuò)展、高性能、公平的自旋鎖。

MCSLock 與 CLHNode的差異

  1. 從代碼實(shí)現(xiàn)來看,CLH比MCS要簡單得多。
  2. 從自旋的條件來看,CLH是在前驅(qū)節(jié)點(diǎn)的屬性上自旋,而MCS是在本地屬性變量上自旋。
  • MCSLock:while (!qnode.locked)
  • CLHNode:while (pre.locked)
  1. 從鏈表隊(duì)列來看,CLHNode不直接持有前驅(qū)節(jié)點(diǎn),CLH鎖釋放時(shí)只需要改變自己的屬性;MCSNode直接持有后繼節(jié)點(diǎn),MCS鎖釋放需要改變后繼節(jié)點(diǎn)的屬性。
  2. CLH鎖釋放時(shí)只需要改變自己的屬性,MCS鎖釋放則需要改變后繼節(jié)點(diǎn)的屬性。
  public class MCSLock implements Lock {
    // 尾結(jié)點(diǎn)
    private AtomicReference<QNode> tail;
    // 當(dāng)前節(jié)點(diǎn)
    private ThreadLocal<QNode> myNode;

    public MCSLock() {
        tail = new AtomicReference<>(null);
        myNode = ThreadLocal.withInitial(QNode::new);
    }
    @Override
    public void lock() {
        // 獲取當(dāng)前節(jié)點(diǎn)
        QNode qnode = myNode.get();
        // 賦值當(dāng)前節(jié)點(diǎn) 并返回舊值(即上個(gè)節(jié)點(diǎn))
        QNode preNode = tail.getAndSet(qnode);
        // 上個(gè)節(jié)點(diǎn)不為空 說明有線程持有資源
        if (preNode != null){
            // 設(shè)置當(dāng)前節(jié)點(diǎn)設(shè)置為false
            qnode.locked = false;
            // 將上個(gè)節(jié)點(diǎn)的指針 指向當(dāng)前節(jié)點(diǎn)
            preNode.next = qnode;
            // 等待上個(gè)節(jié)點(diǎn)執(zhí)行完畢
            while (!qnode.locked) {
            }
        }
        // 設(shè)置當(dāng)前節(jié)點(diǎn)為持有資源進(jìn)程
        qnode.locked = true;
    }

    @Override
    public void unlock() {
        // 獲取當(dāng)前節(jié)點(diǎn)
        QNode qnode = myNode.get();
        if (qnode.next == null) {
            //后面沒有等待線程的情況
            if (tail.compareAndSet(qnode, null)) {
                //真的沒有等待線程,則直接返回,不需要通知
                return;
            }
            // if (tail.compareAndSet(qnode, null)) return false 說明已經(jīng)進(jìn)入了另外一個(gè)線程
            while (qnode.next == null) {
            }
        }
        //后面有等待線程,則通知后面的線程
        qnode.next.locked = true;
        qnode.next = null;
    }

    private class QNode {
        /**
         * 是否被qNode所屬線程鎖定
         */
        private volatile boolean locked = false;
        /**
         * 與CLHLock相比,多了這個(gè)真正的next
         */
        private volatile QNode next = null;
    }
}
優(yōu)點(diǎn):

申請(qǐng)線程只在本地變量上自旋,直接前驅(qū)負(fù)責(zé)通知其結(jié)束自旋,從而極大地減少了不必要的處理器緩存同步的次數(shù),降低了總線和內(nèi)存的開銷,解決NUMA系統(tǒng)結(jié)構(gòu)的思路是MCS隊(duì)列鎖。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,582評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,801評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,223評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,442評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,976評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,800評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,996評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評(píng)論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,233評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評(píng)論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,702評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,991評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容