Java並發包源碼學習之AQS框架(二)CLH lock queue和自旋鎖


上一篇文章提到AQS是基於CLH lock queue,那么什么是CLH lock queue,說復雜很復雜說簡單也簡單, 所謂大道至簡:

CLH lock queue其實就是一個FIFO的隊列,隊列中的每個結點(線程)只要等待其前繼釋放鎖就可以了。

AbstractQueuedSynchronizer是通過一個內部類Node來實現CLH lock queue的一個變種,但基本原理是類似的。

在介紹Node類之前,我們來介紹下Spin Lock,通常就是用CLH lock queue來實現自旋鎖,所謂自旋鎖簡單來說就是線程通過循環來等待而不是睡眠。 Talk 再多不如 show code:

class ClhSpinLock {
    private final ThreadLocal<Node> prev;
    private final ThreadLocal<Node> node;
    private final AtomicReference<Node> tail = new AtomicReference<Node>(new Node());

    public ClhSpinLock() {
        this.node = new ThreadLocal<Node>() {
            protected Node initialValue() {
                return new Node();
            }
        };

        this.prev = new ThreadLocal<Node>() {
            protected Node initialValue() {
                return null;
            }
        };
    }

    public void lock() {
        final Node node = this.node.get();
        node.locked = true;
        // 一個CAS操作即可將當前線程對應的節點加入到隊列中,
        // 並且同時獲得了前繼節點的引用,然后就是等待前繼釋放鎖
        Node pred = this.tail.getAndSet(node);
        this.prev.set(pred);
        while (pred.locked) {// 進入自旋
        }
    }

    public void unlock() {
        final Node node = this.node.get();
        node.locked = false;
        this.node.set(this.prev.get());
    }

    private static class Node {
        private volatile boolean locked;
    }
}

上面的代碼中線程巧妙的通過ThreadLocal保存了當前結點和前繼結點的引用,自旋就是lock中的while循環。 總的來說這種實現的好處是保證所有等待線程的公平競爭,而且沒有競爭同一個變量,因為每個線程只要等待自己的前繼釋放就好了。 而自旋的好處是線程不需要睡眠和喚醒,減小了系統調用的開銷。

public static void main(String[] args) {
    final ClhSpinLock lock = new ClhSpinLock();
    lock.lock();

    for (int i = 0; i < 10; i++) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                System.out.println(Thread.currentThread().getId() + " acquired the lock!");
                lock.unlock();
            }
        }).start();
        Thread.sleep(100);
    }

    System.out.println("main thread unlock!");
    lock.unlock();
}

上面代碼的運行的結果應該跟上一篇文章中的完全一樣。

ClhSpinLock的Node類實現很簡單只有一個布爾值,AbstractQueuedSynchronizer$Node的實現稍微復雜點,大概是這樣的:

     +------+  prev +-----+       +-----+
head |      | <---- |     | <---- |     |  tail
     +------+       +-----+       +-----+
  • head:頭指針
  • tail:尾指針
  • prev:指向前繼的指針
  • next:這個指針圖中沒有畫出來,它跟prev相反,指向后繼

關鍵不同就是next指針,這是因為AQS中線程不是一直在自旋的,而可能會反復的睡眠和喚醒,這就需要前繼釋放鎖的時候通過next 指針找到其后繼將其喚醒,也就是AQS的等待隊列中后繼是被前繼喚醒的。AQS結合了自旋和睡眠/喚醒兩種方法的優點。

其中線程的睡眠和喚醒就是用到我下一篇文章將要講到的LockSupport

最后提一點,上面的ClhSpinLock類中還有一個關鍵的點就是lock方法中注釋的地方:

一個CAS操作即可將當前線程對應的節點加入到隊列中,並獲取到其前繼。

實際上可以說整個AQS框架都是建立在CAS的基礎上的,這些原子操作是多線程競爭的核心地帶,AQS中很多繞來繞去的代碼都是為了 減少競爭。我會在后面AbstractQueuedSynchronizer源碼分析中做詳細介紹。

 


免責聲明!

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



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