Java自旋鎖的幾種實現


什么是自旋鎖

自旋鎖是指當一個線程嘗試獲取某個鎖時,如果該鎖已被其他線程占用,就一直循環檢測鎖是否被釋放,而不是進入線程掛起或睡眠狀態。

為什么要使用自旋鎖

多個線程對同一個變量一直使用CAS操作,那么會有大量修改操作,從而產生大量的緩存一致性流量,因為每一次CAS操作都會發出廣播通知其他處理器,從而影響程序的性能。

線程自旋與線程阻塞

阻塞的缺點顯而易見,線程一旦進入阻塞(Block),再被喚醒的代價比較高,性能較差。自旋的優點是線程還是Runnable的,只是在執行空代碼。當然一直自旋也會白白消耗計算資源,所以常見的做法是先自旋一段時間,還沒拿到鎖就進入阻塞。JVM在處理synchrized實現時就是采用了這種折中的方案,並提供了調節自旋的參數。

SpinLock簡單自旋鎖(可重入)

spin-lock 是一種基於test-and-set操作的鎖機制。
test_and_set是一個原子操作,讀取lock,查看lock值,如果是0,設置其為1,返回0。如果是lock值為1, 直接返回1。這里lock的值0和1分別表示無鎖和有鎖。由於test_and_set的原子性,不會同時有兩個進程/線程同時進入該方法, 整個方法無須擔心並發操作導致的數據不一致。
這里用AtomicReference是為了使用它的原子性的compareAndSet方法(CAS操作),解決了多線程並發操作導致數據不一致的問題,確保其他線程可以看到鎖的真實狀態。

  • 缺點:

    • CAS操作需要硬件的配合;
    • 保證各個CPU的緩存(L1、L2、L3、跨CPU Socket、主存)的數據一致性,通訊開銷很大,在多處理器系統上更嚴重;
    • 沒法保證公平性,不保證等待進程/線程按照FIFO順序獲得鎖。
public class SpinLock implements Lock {
    /**
     *  use thread itself as  synchronization state
     *  使用Owner Thread作為同步狀態,比使用一個簡單的boolean flag可以攜帶更多信息
     */
    private AtomicReference<Thread> owner = new AtomicReference<>();
    /**
     * reentrant count of a thread, no need to be volatile
     */
    private int count = 0;

    @Override
    public void lock() {
        Thread t = Thread.currentThread();
        // if re-enter, increment the count.
        if (t == owner.get()) {
            ++count;
            return;
        }
        //spin
        while (owner.compareAndSet(null, t)) {
        }
    }

    @Override
    public void unlock() {
        Thread t = Thread.currentThread();
        //only the owner could do unlock;
        if (t == owner.get()) {
            if (count > 0) {
                // reentrant count not zero, just decrease the counter.
                --count;
            } else {
                // compareAndSet is not need here, already checked
                owner.set(null);
            }
        }
    }
}

TicketLock

Ticket Lock 是為了解決上面的公平性問題,類似於現實中銀行櫃台的排隊叫號:鎖擁有一個服務號,表示正在服務的線程,還有一個排隊號;每個線程嘗試獲取鎖之前先拿一個排隊號,然后不斷輪詢鎖的當前服務號是否是自己的排隊號,如果是,則表示自己擁有了鎖,不是則繼續輪詢。

當線程釋放鎖時,將服務號加1,這樣下一個線程看到這個變化,就退出自旋。

public class TicketLock implements Lock {
    private AtomicInteger serviceNum = new AtomicInteger(0);
    private AtomicInteger ticketNum = new AtomicInteger(0);
    private final ThreadLocal<Integer> myNum = new ThreadLocal<>();

    @Override
    public void lock() {
        myNum.set(ticketNum.getAndIncrement());
        while (serviceNum.get() != myNum.get()) {
        }
    }

    @Override
    public void unlock() {
        serviceNum.compareAndSet(myNum.get(), myNum.get() + 1);
        myNum.remove();
    }
}
  • 缺點:
    Ticket Lock 雖然解決了公平性的問題,但是多處理器系統上,每個進程/線程占用的處理器都在讀寫同一個變量serviceNum ,每次讀寫操作都必須在多個處理器緩存之間進行緩存同步,這會導致繁重的系統總線和內存的流量,大大降低系統整體的性能。

下面介紹的CLH鎖和MCS鎖都是為了解決這個問題的。

CLHLock

CLH的發明人是:Craig,Landin and Hagersten。是一種基於鏈表的可擴展、高性能、公平的自旋鎖,申請線程只在本地變量上自旋,它不斷輪詢前驅的狀態,如果發現前驅釋放了鎖就結束自旋。

CLH隊列中的結點QNode中含有一個locked字段,該字段若為true表示該線程需要獲取鎖,且不釋放鎖,為false表示線程釋放了鎖。結點之間是通過隱形的鏈表相連,之所以叫隱形的鏈表是因為這些結點之間沒有明顯的next指針,而是通過preNode所指向的結點的變化情況來影響myNode的行為。CLHLock上還有一個尾指針,始終指向隊列的最后一個結點。CLHLock的類圖如下所示:

當一個線程需要獲取鎖時,會創建一個新的QNode,將其中的locked設置為true表示需要獲取鎖,然后線程對tail域調用getAndSet方法,使自己成為隊列的尾部,同時獲取一個指向其前趨的引用preNode,然后該線程就在前趨結點的locked字段上自旋,直到前趨結點釋放鎖。當一個線程需要釋放鎖時,將當前結點的locked域設置為false,同時回收前趨結點。如下圖所示,線程A需要獲取鎖,其myNode域為true,些時tail指向線程A的結點,然后線程B也加入到線程A后面,tail指向線程B的結點。然后線程A和B都在它的preNode域上旋轉,一旦它的preNode結點的locked字段變為false,它就可以獲取鎖。明顯線程A的preNode locked域為false,此時線程A獲取到了鎖。

實現如下:

public class CLHLock implements Lock {

    /**
     * 鎖等待隊列的尾部
     */
    private AtomicReference<QNode> tail;
    private ThreadLocal<QNode> preNode;
    private ThreadLocal<QNode> myNode;

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

    @Override
    public void lock() {
        QNode qnode = myNode.get();
        //設置自己的狀態為locked=true表示需要獲取鎖
        qnode.locked = true;
        //鏈表的尾部設置為本線程的qNode,並將之前的尾部設置為當前線程的preNode
        QNode pre = tail.getAndSet(qnode);
        preNode.set(pre);
        if(pre != null) {
            //當前線程在前驅節點的locked字段上旋轉,直到前驅節點釋放鎖資源
            while (pre.locked) {
            }
        }
    }

    @Override
    public void unlock() {
        QNode qnode = myNode.get();
        //釋放鎖操作時將自己的locked設置為false,可以使得自己的后繼節點可以結束自旋
        qnode.locked = false;
        //回收自己這個節點,從虛擬隊列中刪除
        //將當前節點引用置為自己的preNode,那么下一個節點的preNode就變為了當前節點的preNode,這樣就將當前節點移出了隊列
        myNode.set(preNode.get());
    }

    private class QNode {
        /**
         * true表示該線程需要獲取鎖,且不釋放鎖,為false表示線程釋放了鎖,且不需要鎖
         */
        private volatile boolean locked = false;
    }
}

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

MCSLock

MCS 來自於其發明人名字的首字母: John Mellor-Crummey和Michael Scott。是一種基於鏈表的可擴展、高性能、公平的自旋鎖,申請線程只在本地變量上自旋,直接前驅負責通知其結束自旋,從而極大地減少了不必要的處理器緩存同步的次數,降低了總線和內存的開銷。

public class MCSLock implements Lock {
    private AtomicReference<QNode> tail;
    private ThreadLocal<QNode> myNode;

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

    @Override
    public void lock() {
        QNode qnode = myNode.get();
        QNode preNode = tail.getAndSet(qnode);
        if (preNode != null) {
            qnode.locked = false;
            preNode.next = qnode;
            //wait until predecessor gives up the lock
            while (!qnode.locked) {
            }
        }
        qnode.locked = true;
    }

    @Override
    public void unlock() {
        QNode qnode = myNode.get();
        if (qnode.next == null) {
            //后面沒有等待線程的情況
            if (tail.compareAndSet(qnode, null)) {
                //真的沒有等待線程,則直接返回,不需要通知
                return;
            }
            //wait until predecessor fills in its next field
            // 突然有人排在自己后面了,可能還不知道是誰,下面是等待后續者
            while (qnode.next == null) {
            }
        }
        //后面有等待線程,則通知后面的線程
        qnode.next.locked = true;
        qnode.next = null;
    }

    private class QNode {
        /**
         * 是否被qNode所屬線程鎖定
         */
        private volatile boolean locked = false;
        /**
         * 與CLHLock相比,多了這個真正的next
         */
        private volatile QNode next = null;
    }
}

CLH鎖 與 MCS鎖 的比較

  • 差異:

    • 從代碼實現來看,CLH比MCS要簡單得多。
    • CLH是在前趨結點的locked域上自旋等待,而MCS是在自己的結點的locked域上自旋等待。正因為如此,它解決了CLH在NUMA系統架構中獲取locked域狀態內存過遠的問題。
    • 從鏈表隊列來看,CLHNode不直接持有前驅節點,CLH鎖釋放時只需要改變自己的屬性;MCSNode直接持有后繼節點,MCS鎖釋放需要改變后繼節點的屬性。
    • CLH鎖釋放時只需要改變自己的屬性,MCS鎖釋放則需要改變后繼節點的屬性。


免責聲明!

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



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