JUC-(9)CLHLock實現(xiàn)

簡介

在AQS的源碼中有介紹到一種叫CLH的隊列(AQS中使用的是一種變種).CLH它是一種基于單向鏈表實現(xiàn)的隊列,即后一個節(jié)點保存前一個節(jié)點的引用形成單向鏈接.我們可以使用CLH來實現(xiàn)一個高效的自旋鎖.每個線程請求鎖時,都先判斷它的前節(jié)點是否需要鎖,如果前節(jié)點需要鎖則自己自旋等待.如果前節(jié)點不需要鎖則獲取鎖成功.
正式因為它的這種實現(xiàn)方式,它具有先來先獲取的公平性.而它的核心基于一個CAS操作來實現(xiàn)具有很高的性能.但因為其自旋等待,所以不太適合占用鎖時間過長的場景,因為自旋是會需要消耗CPU資源的.

簡單實現(xiàn)

/**
 * 聲明鎖接口
 */
public interface Lock {
    /**
     * 獲取鎖
     */
    void lock();

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

/**
 * 定義Node
 */
public class CLHNode {
    /**
     * 使用volatile修飾保證可見性.
     */
    volatile boolean locked;
}

/**
 * CLHLock實現(xiàn)
 */
public class CLHLock implements Lock{
    /**
     * 保存當前節(jié)點,一個線程一個
     */
    private final ThreadLocal<CLHNode> node;
    /**
     * 尾節(jié)點.所有線程共享一個.支持CAS操作
     */
    private final AtomicReference<CLHNode> tail;
    /**
     * 父節(jié)點.一個線程一個
     */
    private final ThreadLocal<CLHNode> pred;

    public CLHLock() {
        this.node = ThreadLocal.withInitial(CLHNode::new);
        this.tail = new AtomicReference<>(new CLHNode());
        this.pred = new ThreadLocal<>();
    }

    @Override
    public void lock() {
        //獲取當前節(jié)點
        CLHNode current = node.get();
        //將當前節(jié)點設置成需要鎖
        current.locked = true;
        //將當前節(jié)點設置成尾節(jié)點,CAS操作
        CLHNode currentPred = tail.getAndSet(current);
        //設置當前節(jié)點的前節(jié)點
        pred.set(currentPred);
        //查看當前節(jié)點是否需要鎖,如果需要則自旋等待
        while (currentPred.locked){

        }
        //獲取鎖成功
        System.out.println(Thread.currentThread().getName()+":獲取到鎖");
    }

    @Override
    public void unlock() {
        //獲取當前節(jié)點
        CLHNode current = node.get();
        //將當前節(jié)點locked設置成false,它后面的節(jié)點將會推出自旋
        current.locked = false;
        //將當前節(jié)點設置成前節(jié)點
        node.set(pred.get());
    }
}

/**
 * 使用測試
 */
public class App {

    public static void main(String[] args) {
        Lock lock = new CLHLock();

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                lock.lock();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock.unlock();
            }
        };

        Thread t1 = new Thread(runnable);
        t1.setName("t1");
        t1.start();

        Thread t2 = new Thread(runnable);
        t2.setName("t2");
        t2.start();

        lock.lock();
        lock.unlock();

    }
}
  • 獲取鎖:每個線程獲取鎖時都自己節(jié)點的locked設置成true代表自己需要鎖,然后將自己節(jié)點設置成尾節(jié)點tail(這里使用AtomicReference來實現(xiàn)一個CAS操作),并獲取到當前節(jié)點的前置節(jié)點.接著獲取的前置節(jié)點設置成自己的前置節(jié)點.然后就是判斷通過前置節(jié)點的locked屬性來判斷前置節(jié)點是否需要鎖.這里需要注意的是locked屬性需要使用volatile修飾來保證其他線程修改它的值而當前線程能感知到該值的變化.如果去掉volatile可能導致當前線程一直自旋無法獲取到鎖.
  • 釋放鎖:釋放鎖就比較好理解了,直接獲取到當前節(jié)點(這就是為什么使用ThreadLocal來保存當前節(jié)點的值了).然后將當前節(jié)點的locked設置成false,而這個操作將會使該節(jié)點的后置節(jié)點退出自旋獲取到鎖.

關于解鎖操作中的node.set(pred.get()).它主要是為了防止獲取鎖解鎖之后再次獲取鎖無法獲取鎖的bug.如果去掉該行代碼,第一次獲取鎖當前節(jié)點已經(jīng)是tail節(jié)點了,然后再次獲取鎖.再次將當前節(jié)點作為尾節(jié)點.即當前節(jié)點的頭節(jié)點和當前節(jié)點是同一個節(jié)點.如果這個時候設置lockedtrue,這將導致當前節(jié)點再判斷前節(jié)點是否需要鎖時一直都是true.線程將在獲取鎖時無限自旋等待.

總結(jié)

總體來說使用這種方式的有點如下:

  • 簡單高效.
  • 支持FIFO公平性.
  • 自旋等待避免線程切換的性能損耗

同樣它也有下面的缺點:

  • 不可重入.即獲取鎖之后必須釋放鎖才能再次獲取鎖.
  • 鎖占用時間過長導致CPU空轉(zhuǎn).
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末狂秘,一起剝皮案震驚了整個濱河市统阿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌堵未,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盏触,死亡現(xiàn)場離奇詭異渗蟹,居然都是意外死亡,警方通過查閱死者的電腦和手機赞辩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進店門雌芽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人诗宣,你說我怎么就攤上這事膘怕。” “怎么了召庞?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵岛心,是天一觀的道長。 經(jīng)常有香客問我篮灼,道長忘古,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任诅诱,我火速辦了婚禮髓堪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己干旁,他們只是感情好驶沼,可當我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著争群,像睡著了一般回怜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上换薄,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天玉雾,我揣著相機與錄音,去河邊找鬼轻要。 笑死复旬,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的冲泥。 我是一名探鬼主播驹碍,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼柏蘑!你這毒婦竟也來了幸冻?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤咳焚,失蹤者是張志新(化名)和其女友劉穎洽损,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體革半,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡碑定,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年刷允,在試婚紗的時候發(fā)現(xiàn)自己被綠了吱韭。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片畜埋。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡品擎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出忽刽,到底是詐尸還是另有隱情升筏,我是刑警寧澤轻猖,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布外构,位于F島的核電站普泡,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏审编。R本人自食惡果不足惜撼班,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望垒酬。 院中可真熱鬧砰嘁,春花似錦件炉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至板祝,卻和暖如春宫静,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背券时。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留伏伯,地道東北人橘洞。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像说搅,于是被迫代替她去往敵國和親炸枣。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,509評論 2 348