Java鎖機制

Java 鎖分類

  • 從線程是否需要對資源加鎖可以分爲 悲觀鎖 和 樂觀鎖
  • 從資源已被鎖定,線程是否阻塞可以分爲 自旋鎖
  • 從多個線程併發訪問資源,也就是 Synchronized 可以分爲 無鎖、偏向鎖、 輕量級鎖和 重量級鎖
  • 從鎖的公平性進行區分,可以分爲公平鎖 和 非公平鎖
  • 從根據鎖是否重複獲取可以分爲 可重入鎖 和 不可重入鎖
  • 從那個多個線程能否獲取同一把鎖分爲 共享鎖 和 排他鎖

Java 中的 Synchronized 和 ReentrantLock 等獨佔鎖(排他鎖)也是一種悲觀鎖思想的實現,因爲 Synchronzied 和 ReetrantLock 不管是否持有資源,它都會嘗試去加鎖

樂觀鎖

使用場景:讀多寫少

樂觀鎖的實現方案一般來說有兩種:版本號機制 和 CAS實現 。樂觀鎖多適用於多讀的應用類型,這樣可以提高吞吐量。

在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式 CAS 實現的。

自旋鎖

自旋鎖的定義:當一個線程嘗試去獲取某一把鎖的時候,如果這個鎖此時已經被別人獲取(佔用),那麼此線程就無法獲取到這把鎖,該線程將會等待,間隔一段時間後會再次嘗試獲取。這種採用循環加鎖 -> 等待的機制被稱爲自旋鎖(spinlock)。

由於這個原因,操作系統的內核經常使用自旋鎖。但是,如果長時間上鎖的話,自旋鎖會非常耗費性能,它阻止了其他線程的運行和調度。線程持有鎖的時間越長,則持有該鎖的線程將被 OS(Operating System) 調度程序中斷的風險越大。

如果發生中斷情況,那麼其他線程將保持旋轉狀態(反覆嘗試獲取鎖),而持有該鎖的線程並不打算釋放鎖,這樣導致的是結果是無限期推遲,直到持有鎖的線程可以完成並釋放它爲止。

解決上面這種情況一個很好的方式是給自旋鎖設定一個自旋時間,等時間一到立即釋放自旋鎖。在JDK在1.6 引入了適應性自旋鎖,適應性自旋鎖意味着自旋時間不是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖擁有的狀態來決定,基本認爲一個線程上下文切換的時間是最佳的一個時間。

自旋鎖有一個問題:無法保證多線程競爭的公平性

排隊自旋鎖(QueuedSpinlock)

TicketLock

TicketLock 中有兩個 int 類型的數值,開始都是0,第一個值是隊列ticket(隊列票據), 第二個值是 出隊(票據)。隊列票據是線程在隊列中的位置,而出隊票據是現在持有鎖的票證的隊列位置。可能有點模糊不清,簡單來說,就是隊列票據是你取票號的位置,出隊票據是你距離叫號的位置。

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

爲了解決這個問題,MCSLock 和 CLHLock 應運而生。

CLHLock

上面說到TicketLock 是基於隊列的,那麼 CLHLock 就是基於鏈表設計的

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

public class CLHLock {

    public static class CLHNode{
        private volatile boolean isLocked = true;
    }

    // 尾部節點
    private volatile CLHNode tail;
    private static final ThreadLocalLOCAL = new ThreadLocal<>();
    private static final AtomicReferenceFieldUpdaterUPDATER =
            AtomicReferenceFieldUpdater.newUpdater(CLHLock.class,CLHNode.class,"tail");


    public void lock(){
        // 新建節點並將節點與當前線程保存起來
        CLHNode node = new CLHNode();
        LOCAL.set(node);

        // 將新建的節點設置爲尾部節點,並返回舊的節點(原子操作),這裏舊的節點實際上就是當前節點的前驅節點
        CLHNode preNode = UPDATER.getAndSet(this,node);
        if(preNode != null){
            // 前驅節點不爲null表示當鎖被其他線程佔用,通過不斷輪詢判斷前驅節點的鎖標誌位等待前驅節點釋放鎖
            while (preNode.isLocked){

            }
            preNode = null;
            LOCAL.set(node);
        }
        // 如果不存在前驅節點,表示該鎖沒有被其他線程佔用,則當前線程獲得鎖
    }

    public void unlock() {
        // 獲取當前線程對應的節點
        CLHNode node = LOCAL.get();
        // 如果tail節點等於node,則將tail節點更新爲null,同時將node的lock狀態職位false,表示當前線程釋放了鎖
        if (!UPDATER.compareAndSet(this, node, null)) {
            node.isLocked = false;
        }
        node = null;
    }
}

MCSLock

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

public class MCSLock {

    public static class MCSNode {
        volatile MCSNode next;
        volatile boolean isLocked = true;
    }

    private static final ThreadLocalNODE = new ThreadLocal<>();

    // 隊列
    @SuppressWarnings("unused")
    private volatile MCSNode queue;

    private static final AtomicReferenceFieldUpdaterUPDATE =
            AtomicReferenceFieldUpdater.newUpdater(MCSLock.class,MCSNode.class,"queue");


    public void lock(){
        // 創建節點並保存到ThreadLocal中
        MCSNode currentNode = new MCSNode();
        NODE.set(currentNode);

        // 將queue設置爲當前節點,並且返回之前的節點
        MCSNode preNode = UPDATE.getAndSet(this, currentNode);
        if (preNode != null) {
            // 如果之前節點不爲null,表示鎖已經被其他線程持有
            preNode.next = currentNode;
            // 循環判斷,直到當前節點的鎖標誌位爲false
            while (currentNode.isLocked) {
            }
        }
    }

    public void unlock() {
        MCSNode currentNode = NODE.get();
        // next爲null表示沒有正在等待獲取鎖的線程
        if (currentNode.next == null) {
            // 更新狀態並設置queue爲null
            if (UPDATE.compareAndSet(this, currentNode, null)) {
                // 如果成功了,表示queue==currentNode,即當前節點後面沒有節點了
                return;
            } else {
                // 如果不成功,表示queue!=currentNode,即當前節點後面多了一個節點,表示有線程在等待
                // 如果當前節點的後續節點爲null,則需要等待其不爲null(參考加鎖方法)
                while (currentNode.next == null) {
                }
            }
        } else {
            // 如果不爲null,表示有線程在等待獲取鎖,此時將等待線程對應的節點鎖狀態更新爲false,同時將當前線程的後繼節點設爲null
            currentNode.next.isLocked = false;
            currentNode.next = null;
        }
    }
}

CLHLock 和 MCSLock

  • 都是基於鏈表,不同的是CLHLock是基於隱式鏈表,沒有真正的後續節點屬性,MCSLock是顯示鏈表,有一個指向後續節點的屬性。
  • 將獲取鎖的線程狀態藉助節點(node)保存,每個線程都有一份獨立的節點,這樣就解決了TicketLock多處理器緩存同步的問題。
發佈了60 篇原創文章 · 獲贊 12 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章