樂觀鎖,悲觀鎖介紹及常見鎖算法

鎖類型

悲觀鎖

事事皆總作最壞的打算,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完後再把資源轉讓給其它線程)。傳統的關係型數據庫裏邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。Java中synchronizedReentrantLock等獨佔鎖就是悲觀鎖思想的實現。

樂觀鎖

事事皆總作最好的打算,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號機制和CAS算法實現。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫提供的類似於write_condition機制,其實都是提供的樂觀鎖。在Java中 java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。

應用場景

樂觀鎖適用於寫比較少的情況下(多讀場景),即衝突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果是多寫的情況,一般會經常產生衝突,這就會導致上層應用會不斷的進行retry,這樣反倒是降低了性能,所以一般多寫的場景下用悲觀鎖就比較合適。

常見鎖算法

樂觀鎖常見算法

CAS算法

cas 鎖(compare and swap) 對比交換

  • CAS算法涉及到三個操作數
    • 需要讀寫的內存值V
    • 進行比較的值 A
    • 擬寫入的新值 B
      當且僅當 V 的值等於 A時,CAS通過原子方式用新值B來更新V的值,否則不會執行任何操作(比較和替換是一個原子操作)。一般情況下是一個自旋操作,即不斷的重試
  • 問題
    • ABA 問題:
      如果一個變量V初次讀取的時候是A值,並且在準備賦值的時候檢查到它仍然是A值,那我們就能說明它的值沒有被其他線程修改過了嗎?很明顯是不能的,因爲在這段時間它的值可能被改爲其他值,然後又改回A,那CAS操作就會誤認爲它從來沒有被修改過。這個問題被稱爲CAS操作的 "ABA"問題。
      JDK 1.5 以後的 AtomicStampedReference類就提供了此種能力,其中的 compareAndSet方法就是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。
    • 循環時間長開銷大:
      自旋CAS(也就是不成功就一直循環執行直到成功)如果長時間不成功,會給CPU帶來非常大的執行開銷。 如果JVM能支持處理器提供的pause指令那麼效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出循環的時候因內存順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率
    • 只能保證一個共享變量的原子操作:
      CAS 只對單個共享變量有效,當操作涉及跨多個共享變量時 CAS 無效。但是從 JDK 1.5開始,提供了 AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象裏來進行 CAS 操作.所以我們可以使用鎖或者利用 AtomicReference類把多個共享變量合併成一個共享變量來操作。

版本號算法

一般是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新,否則重試更新操作,直到更新成功。

##常見悲觀鎖

Java Synchronized

synchronized是Java中的關鍵字,是一種同步鎖。可修飾實例方法,靜態方法,代碼塊。

ADD

什麼是自旋鎖?

自旋鎖(spinlock):是指當一個線程在獲取鎖的時候,如果鎖已經被其它線程獲取,那麼該線程將循環等待,然後不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖纔會退出循環。 即樂觀鎖的一種實現。

自旋鎖存在的問題

  • 如果某個線程持有鎖的時間過長,就會導致其它等待獲取鎖的線程進入循環等待,消耗CPU。使用不當會造成CPU使用率極高。
  • 無法滿足等待時間最長的線程優先獲取鎖。不公平的鎖就會存在“線程飢餓”問題。

自旋鎖的優點

  • 自旋鎖不會使線程狀態發生切換,一直處於用戶態,即線程一直都是active的;不會使線程進入阻塞狀態,減少了不必要的上下文切換,執行速度快
  • 非自旋鎖在獲取不到鎖的時候會進入阻塞狀態,從而進入內核態,當獲取到鎖的時候需要從內核態恢復,需要線程上下文切換。 (線程被阻塞後便進入內核(Linux)調度狀態,這個會導致系統在用戶態與內核態之間來回切換,嚴重影響鎖的性能)

自旋鎖的變種

MCS鎖

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


import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class MCSLockV2 {
    /**
     * MCS鎖節點
     */
    public static class MCSNodeV2 {
        /**
         * 後繼節點
         */
        volatile MCSNodeV2 next;
        /**
         * 默認狀態爲等待鎖
         */
        volatile boolean blocked = true;
    }

    /**
     * 線程到節點的映射
     */
    private ThreadLocal<MCSNodeV2> currentThreadNode = new ThreadLocal<>();
    /**
     * 指向最後一個申請鎖的MCSNode
     */
    volatile MCSNodeV2 queue;

    /**
     * 原子更新器
     */
    private static final AtomicReferenceFieldUpdater UPDATER = AtomicReferenceFieldUpdater.newUpdater(
            MCSLockV2.class,
            MCSLockV2.MCSNodeV2.class,
            "queue");

    /**
     * MCS獲取鎖操作
     */
    public void lock() {
        MCSNodeV2 cNode = currentThreadNode.get();
        if (cNode == null) {
            System.out.println(String.format("Thread %s 初始化 ", Thread.currentThread().getId()));
            // 初始化節點對象
            cNode = new MCSNodeV2();
            currentThreadNode.set(cNode);
        }
        // 將當前申請鎖的線程置爲queue並返回舊值
        MCSNodeV2 mcsNodeV2 = (MCSNodeV2) UPDATER.getAndSet(this, cNode); // step 1
        if (mcsNodeV2 != null) {
            // 形成鏈表結構(單向)
            mcsNodeV2.next = cNode; // step 2
            // 當前線程處於等待狀態時自旋(MCSNode的blocked初始化爲true)
            // 等待前驅節點主動通知,即將blocked設置爲false,表示當前線程可以獲取到鎖
            while (cNode.blocked) {

            }
        } else {
            // 只有一個線程在使用鎖,沒有前驅來通知它,所以得自己標記自己爲非阻塞 - 表示已經加鎖成功
            cNode.blocked = false;
        }
    }

    /**
     * MCS釋放鎖操作
     */
    public void unlock() {
        // 獲取當前線程對應的節點
        MCSNodeV2 cNode = currentThreadNode.get();

        if (cNode == null || cNode.blocked) {
            // 當前線程對應存在節點
            // 並且
            // 鎖擁有者進行釋放鎖纔有意義 - 當blocked未true時,表示此線程處於等待狀態中,並沒有獲取到鎖,因此沒有權利釋放鎖
            return;
        }

        if (cNode.next == null && !UPDATER.compareAndSet(this, cNode, null)) {
            // 沒有後繼節點的情況,將queue置爲空
            // 如果CAS操作失敗了表示突然有節點排在自己後面了,可能還不知道是誰,下面是等待後續者
            // 這裏之所以要忙等是因爲上述的lock操作中step 1執行完後,step 2可能還沒執行完
            while (cNode.next == null) {

            }
        }

        if (cNode.next != null) {
            // 通知後繼節點可以獲取鎖
            cNode.next.blocked = false;

            // 將當前節點從鏈表中斷開,方便對當前節點進行GC
            cNode.next = null; // for GC
        }

        // 清空當前線程對應的節點信息
        currentThreadNode.remove();

    }

    /**
     * 測試用例
     *
     * @param args
     */
    public static void main(String[] args) {

        final MCSLockV2 lock = new MCSLockV2();

        for (int i = 1; i <= 10; i++) {
            new Thread(generateTask(lock, String.valueOf(i))).start();
        }

    }

    private static Runnable generateTask(final MCSLockV2 lock, final String taskId) {
        return () -> {
            lock.lock();
            try {
                Thread.sleep(3000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(String.format("Thread %s Completed %s ", Thread.currentThread().getId(), taskId));
            lock.unlock();
        };
    }
}
  • MCS鎖的節點對象需要有兩個狀態,next用來維護單向鏈表的結構,blocked用來表示節點的狀態,true表示處於自旋中;false表示加鎖成功
  • MCS鎖的節點狀態blocked的改變是由其前驅節點觸發改變的
  • 加鎖時會更新鏈表的末節點並完成鏈表結構的維護
  • 釋放鎖的時候由於鏈表結構建立的時滯(getAndSet原子方法和鏈表建立整體而言並非原子性),可能存在多線程的干擾,需要使用忙等待保證鏈表結構就緒

CLH鎖

同MCS自旋鎖一樣,CLH也是一種基於單向鏈表(隱式創建)的高性能、公平的自旋鎖,申請加鎖的線程只需要在其前驅節點的本地變量上自旋,從而極大地減少了不必要的處理器緩存同步的次數,降低了總線和內存的開銷。


import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class CLHLockV2 {
    /**
     * CLH鎖節點狀態 - 每個希望獲取鎖的線程都被封裝爲一個節點對象
     */
    private static class CLHNodeV2 {

        /**
         * 默認狀態爲true - 即處於等待狀態或者加鎖成功(換言之,即此節點處於有效的一種狀態)
         */
        volatile boolean active = true;

    }

    /**
     * 隱式鏈表最末等待節點
     */
    private volatile CLHNodeV2 tail = null;
    /**
     * 線程對應CLH節點映射
     */
    private ThreadLocal<CLHNodeV2> currentThreadCacheNode = new ThreadLocal<>();

    /**
     * 原子更新器
     */
    private static final AtomicReferenceFieldUpdater<CLHLockV2, CLHNodeV2> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLockV2.class, CLHNodeV2.class, "tail");

    /**
     * CLH加鎖
     */
    public void lock() {
        CLHNodeV2 cNode = currentThreadCacheNode.get();
        if (cNode == null) {
            cNode = new CLHNodeV2();
            currentThreadCacheNode.set(cNode);
        }
        // 通過這個操作完成隱式鏈表的維護,後繼節點只需要在前驅節點的locked狀態上自旋
        CLHNodeV2 clhNodeV2 = UPDATER.getAndSet(this, cNode);
        if (clhNodeV2 != null) {
            // 自旋等待前驅節點狀態變更 - unlock中進行變更
            while (clhNodeV2.active) {
                //等待unlock 時被釋放
            }
            /**
             * 因爲unlock 後 當前線程已經結束 ,所以顯示的是 阻塞線程的的下一線程的ID
             *
             */
            System.out.println(String.format("釋放鎖%s成功", Thread.currentThread().getId()));
        }
        // 沒有前驅節點表示可以直接獲取到鎖,由於默認獲取鎖狀態爲true,此時可以什麼操作都不執行
        // 能夠執行到這裏表示已經成功獲取到了鎖
    }

    /**
     * CLH釋放鎖
     */
    public void unlock() {
        CLHNodeV2 cNode = currentThreadCacheNode.get();
        // 只有持有鎖的線程才能夠釋放
        if (cNode == null || !cNode.active) {
            return;
        }
        // 從映射關係中移除當前線程對應的節點
        currentThreadCacheNode.remove();
        // 嘗試將tail從currentThread變更爲null,因此當tail不爲currentThread時表示還有線程在等待加鎖
        if (!UPDATER.compareAndSet(this, cNode, null)) {
            // 不僅只有當前線程,還有後續節點線程的情況 - 將當前線程的鎖狀態置爲false,因此其後繼節點的lock自旋操作可以退出
            cNode.active = false;
        }
    }

    /**
     * 用例
     *
     * @param args
     */
    public static void main(String[] args) {
        final CLHLockV2 lock = new CLHLockV2();
        for (int i = 1; i <= 10; i++) {
            new Thread(generateTask(lock, String.valueOf(i))).start();
        }
    }

    private static Runnable generateTask(final CLHLockV2 lock, final String taskId) {
        return () -> {
            lock.lock();

            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }

            System.out.println(String.format("Thread %s Completed %s ", Thread.currentThread().getId(), taskId));
            lock.unlock();
        };
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章