自旋鎖、排隊自旋鎖、MCS鎖、CLH鎖

自旋鎖(Spin lock)

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

自旋鎖適用於鎖保護的臨界區很小的情況,臨界區很小的話,鎖佔用的時間就很短。

簡單的實現

import java.util.concurrent.atomic.AtomicReference;

public class SpinLock {
   private AtomicReference<Thread> owner = new AtomicReference<Thread>();

   public void lock() {
       Thread currentThread = Thread.currentThread();

              // 如果鎖未被佔用,則設置當前線程爲鎖的擁有者
       while (owner.compareAndSet(null, currentThread)) {
       }
   }

   public void unlock() {
       Thread currentThread = Thread.currentThread();

              // 只有鎖的擁有者才能釋放鎖
       owner.compareAndSet(currentThread, null);
   }
}

SimpleSpinLock裏有一個owner屬性持有鎖當前擁有者的線程的引用,如果該引用爲null,則表示鎖未被佔用,不爲null則被佔用。

這裏用AtomicReference是爲了使用它的原子性的compareAndSet方法(CAS操作),解決了多線程併發操作導致數據不一致的問題,確保其他線程可以看到鎖的真實狀態。

缺點

  1. CAS操作需要硬件的配合;
  2. 保證各個CPU的緩存(L1、L2、L3、跨CPU Socket、主存)的數據一致性,通訊開銷很大,在多處理器系統上更嚴重;
  3. 沒法保證公平性,不保證等待進程/線程按照FIFO順序獲得鎖。

Ticket Lock

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

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

簡單的實現

import java.util.concurrent.atomic.AtomicInteger;

public class TicketLock {
   private AtomicInteger serviceNum = new AtomicInteger(); // 服務號
   private AtomicInteger ticketNum = new AtomicInteger(); // 排隊號

   public int lock() {
         // 首先原子性地獲得一個排隊號
         int myTicketNum = ticketNum.getAndIncrement();

              // 只要當前服務號不是自己的就不斷輪詢
       while (serviceNum.get() != myTicketNum) {
       }

       return myTicketNum;
    }

    public void unlock(int myTicket) {
        // 只有當前線程擁有者才能釋放鎖
        int next = myTicket + 1;
        serviceNum.compareAndSet(myTicket, next);
    }
}

缺點

Ticket Lock 雖然解決了公平性的問題,但是多處理器系統上,每個進程/線程佔用的處理器都在讀寫同一個變量serviceNum ,每次讀寫操作都必須在多個處理器緩存之間進行緩存同步,這會導致繁重的系統總線和內存的流量,大大降低系統整體的性能。

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

MCS 來自於其發明人名字的首字母: John Mellor-Crummey和Michael Scott。

CLH的發明人是:Craig,Landin and Hagersten。

CLH鎖

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

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class CLHLock {
    public static class CLHNode {
        private boolean isLocked = true; // 默認是在等待鎖
    }

    @SuppressWarnings("unused" )
    private volatile CLHNode tail ;
    private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater
                  . newUpdater(CLHLock.class, CLHNode .class , "tail" );

    public void lock(CLHNode currentThreadCLHNode) {
        CLHNode preNode = UPDATER.getAndSet( this, currentThreadCLHNode); // 轉載人註釋: 把this裏的"tail" 值設置成currentThreadCLHNode
        if(preNode != null) {//已有線程佔用了鎖,進入自旋
            while(preNode.isLocked ) {
            }
        }
    }

    public void unlock(CLHNode currentThreadCLHNode) {
        // 如果隊列裏只有當前線程,則釋放對當前線程的引用(for GC)。
        if (!UPDATER .compareAndSet(this, currentThreadCLHNode, null)) {
            // 還有後續線程
            currentThreadCLHNode. isLocked = false ;// 改變狀態,讓後續線程結束自旋
        }
    }
}

MCS鎖

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

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class MCSLock {
    public static class MCSNode {
        MCSNode next;
        boolean isLocked = true; // 默認是在等待鎖
    }

    volatile MCSNode queue ;// 指向最後一個申請鎖的MCSNode
    private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater
                  . newUpdater(MCSLock.class, MCSNode. class, "queue" );

    public void lock(MCSNode currentThreadMcsNode) {
        MCSNode predecessor = UPDATER.getAndSet(this, currentThreadMcsNode);// step 1
        if (predecessor != null) {
            predecessor.next = currentThreadMcsNode;// step 2

            while (currentThreadMcsNode.isLocked ) {// step 3
            }
        }
    }

    public void unlock(MCSNode currentThreadMcsNode) {
        if ( UPDATER.get( this ) == currentThreadMcsNode) {// 鎖擁有者進行釋放鎖纔有意義
            if (currentThread.next == null) {// 檢查是否有人排在自己後面
                if (UPDATER.compareAndSet(this, currentThreadMcsNode, null)) {// step 4
                    // compareAndSet返回true表示確實沒有人排在自己後面
                    return;
                } else {
                    // 突然有人排在自己後面了,可能還不知道是誰,下面是等待後續者
                    // 這裏之所以要忙等是因爲:step 1執行完後,step 2可能還沒執行完
                    while (currentThreadMcsNode.next == null) { // step 5
                    }
                }
            }

            currentThreadMcsNode.next.isLocked = false;
            currentThreadMcsNode.next = null;// for GC
        }
    }
}
CLH鎖 與 MCS鎖 的比較

下圖是CLH鎖和MCS鎖隊列圖示:
CLH-MCS-SpinLock

差異:

  1. 從代碼實現來看,CLH比MCS要簡單得多。
  2. 從自旋的條件來看,CLH是在前驅節點的屬性上自旋,而MCS是在本地屬性變量上自旋
  3. 從鏈表隊列來看,CLH的隊列是隱式的,CLHNode並不實際持有下一個節點;MCS的隊列是物理存在的。
  4. CLH鎖釋放時只需要改變自己的屬性,MCS鎖釋放則需要改變後繼節點的屬性。

注意:這裏實現的鎖都是獨佔的,且不能重入的。

發佈了29 篇原創文章 · 獲贊 19 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章