看完你就明白的鎖系列之自旋鎖

看完你就明白的鎖系列之自旋鎖

在上一篇文章 看完你就應該能明白的悲觀鎖和樂觀鎖 中咱們已經學習到了什麼是悲觀鎖和樂觀鎖、悲觀鎖和樂觀鎖的實現、優缺點分別是什麼。其中樂觀鎖的實現之一 CAS 算法中提到了一個自旋鎖的概念,爲了全面理解 CAS 算法就首先須要瞭解一下自旋鎖 是什麼,自旋鎖的適用場景和優缺點分別是什麼,彆着急,下面爲你一一列舉。html

自旋鎖的提出背景

因爲在多處理器環境中某些資源的有限性,有時須要互斥訪問(mutual exclusion),這時候就須要引入鎖的概念,只有獲取了鎖的線程纔可以對資源進行訪問,因爲多線程的核心是CPU的時間分片,因此同一時刻只能有一個線程獲取到鎖。那麼就面臨一個問題,那麼沒有獲取到鎖的線程應該怎麼辦?java

一般有兩種處理方式:一種是沒有獲取到鎖的線程就一直循環等待判斷該資源是否已經釋放鎖,這種鎖叫作自旋鎖,它不用將線程阻塞起來(NON-BLOCKING);還有一種處理方式就是把本身阻塞起來,等待從新調度請求,這種叫作互斥鎖node

什麼是自旋鎖

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

file

自旋鎖的原理

自旋鎖的原理比較簡單,若是持有鎖的線程能在短期內釋放鎖資源,那麼那些等待競爭鎖的線程就不須要作內核態和用戶態之間的切換進入阻塞狀態,它們只須要等一等(自旋),等到持有鎖的線程釋放鎖以後便可獲取,這樣就避免了用戶進程和內核切換的消耗。緩存

由於自旋鎖避免了操做系統進程調度和線程切換,因此自旋鎖一般適用在時間比較短的狀況下。因爲這個緣由,操做系統的內核常用自旋鎖。可是,若是長時間上鎖的話,自旋鎖會很是耗費性能,它阻止了其餘線程的運行和調度。線程持有鎖的時間越長,則持有該鎖的線程將被 OS(Operating System) 調度程序中斷的風險越大。若是發生中斷狀況,那麼其餘線程將保持旋轉狀態(反覆嘗試獲取鎖),而持有該鎖的線程並不打算釋放鎖,這樣致使的是結果是無限期推遲,直到持有鎖的線程能夠完成並釋放它爲止。多線程

解決上面這種狀況一個很好的方式是給自旋鎖設定一個自旋時間,等時間一到當即釋放自旋鎖。自旋鎖的目的是佔着CPU資源不進行釋放,等到獲取鎖當即進行處理。可是如何去選擇自旋時間呢?若是自旋執行時間太長,會有大量的線程處於自旋狀態佔用 CPU 資源,進而會影響總體系統的性能。所以自旋的週期選的額外重要!JDK在1.6 引入了適應性自旋鎖,適應性自旋鎖意味着自旋時間不是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖擁有的狀態來決定,基本認爲一個線程上下文切換的時間是最佳的一個時間。性能

自旋鎖的優缺點

自旋鎖儘量的減小線程的阻塞,這對於鎖的競爭不激烈,且佔用鎖時間很是短的代碼塊來講性能能大幅度的提高,由於自旋的消耗會小於線程阻塞掛起再喚醒的操做的消耗,這些操做會致使線程發生兩次上下文切換!學習

可是若是鎖的競爭激烈,或者持有鎖的線程須要長時間佔用鎖執行同步塊,這時候就不適合使用自旋鎖了,由於自旋鎖在獲取鎖前一直都是佔用 cpu 作無用功,佔着 XX 不 XX,同時有大量線程在競爭一個鎖,會致使獲取鎖的時間很長,線程自旋的消耗大於線程阻塞掛起操做的消耗,其它須要 cpu 的線程又不能獲取到 cpu,形成 cpu 的浪費。因此這種狀況下咱們要關閉自旋鎖。this

自旋鎖的實現

下面咱們用Java 代碼來實現一個簡單的自旋鎖操作系統

public class SpinLockTest {

    private AtomicBoolean available = new AtomicBoolean(false);

    public void lock(){

        // 循環檢測嘗試獲取鎖
        while (!tryLock()){
            // doSomething...
        }

    }

    public boolean tryLock(){
        // 嘗試獲取鎖,成功返回true,失敗返回false
        return available.compareAndSet(false,true);
    }

    public void unLock(){
        if(!available.compareAndSet(true,false)){
            throw new RuntimeException("釋放鎖失敗");
        }
    }

}

這種簡單的自旋鎖有一個問題:沒法保證多線程競爭的公平性。對於上面的SpinlockTest,當多個線程想要獲取鎖時,誰最早將available設爲false誰就能最早得到鎖,這可能會形成某些線程一直都未獲取到鎖形成線程飢餓。就像咱們下課後蜂擁的跑向食堂,下班後蜂擁地擠向地鐵,一般咱們會採起排隊的方式解決這樣的問題,相似地,咱們把這種鎖叫排隊自旋鎖(QueuedSpinlock)。計算機科學家們使用了各類方式來實現排隊自旋鎖,如TicketLock,MCSLock,CLHLock。接下來咱們分別對這幾種鎖作個大體的介紹。

TicketLock

在計算機科學領域中,TicketLock 是一種同步機制或鎖定算法,它是一種自旋鎖,它使用ticket 來控制線程執行順序。

就像票據隊列管理系統同樣。麪包店或者服務機構(例如銀行)都會使用這種方式來爲每一個先到達的顧客記錄其到達的順序,而不用每次都進行排隊。一般,這種地點都會有一個分配器(叫號器,掛號器等等都行),先到的人須要在這個機器上取出本身如今排隊的號碼,這個號碼是按照自增的順序進行的,旁邊還會有一個標牌顯示的是正在服務的標誌,這一般是表明目前正在服務的隊列號,當前的號碼完成服務後,標誌牌會顯示下一個號碼能夠去服務了。

像上面系統同樣,TicketLock 是基於先進先出(FIFO) 隊列的機制。它增長了鎖的公平性,其設計原則以下:TicketLock 中有兩個 int 類型的數值,開始都是0,第一個值是隊列ticket(隊列票據), 第二個值是 出隊(票據)。隊列票據是線程在隊列中的位置,而出隊票據是如今持有鎖的票證的隊列位置。可能有點模糊不清,簡單來講,就是隊列票據是你取票號的位置,出隊票據是你距離叫號的位置。如今應該明白一些了吧。

當叫號叫到你的時候,不能有相同的號碼同時辦業務,必須只有一我的能夠去辦,辦完後,叫號機叫到下一我的,這就叫作原子性。你在辦業務的時候不能被其餘人所幹擾,並且不可能會有兩個持有相同號碼的人去同時辦業務。而後,下一我的看本身的號是否和叫到的號碼保持一致,若是一致的話,那麼就輪到你去辦業務,不然只能繼續等待。上面這個流程的關鍵點在於,每一個辦業務的人在辦完業務以後,他必須丟棄本身的號碼,叫號機才能繼續叫到下面的人,若是這我的沒有丟棄這個號碼,那麼其餘人只能繼續等待。下面來實現一下這個票據排隊方案

public class TicketLock {

    // 隊列票據(當前排隊號碼)
    private AtomicInteger queueNum = new AtomicInteger();

    // 出隊票據(當前需等待號碼)
    private AtomicInteger dueueNum = new AtomicInteger();

    // 獲取鎖:若是獲取成功,返回當前線程的排隊號
    public int lock(){
        int currentTicketNum = dueueNum.incrementAndGet();
        while (currentTicketNum != queueNum.get()){
            // doSomething...
        }
        return currentTicketNum;
    }

    // 釋放鎖:傳入當前排隊的號碼
    public void unLock(int ticketNum){
        queueNum.compareAndSet(ticketNum,ticketNum + 1);
    }

}

每次叫號機在叫號的時候,都會判斷本身是否是被叫的號,而且每一個人在辦完業務的時候,叫號機根據在當前號碼的基礎上 + 1,讓隊列繼續往前走。

可是上面這個設計是有問題的,由於得到本身的號碼以後,是能夠對號碼進行更改的,這就形成系統紊亂,鎖不能及時釋放。這時候就須要有一個能確保每一個人按會着本身號碼排隊辦業務的角色,在得知這一點以後,咱們從新設計一下這個邏輯

public class TicketLock2 {

    // 隊列票據(當前排隊號碼)
    private AtomicInteger queueNum = new AtomicInteger();

    // 出隊票據(當前需等待號碼)
    private AtomicInteger dueueNum = new AtomicInteger();

    private ThreadLocal<Integer> ticketLocal = new ThreadLocal<>();

    public void lock(){
        int currentTicketNum = dueueNum.incrementAndGet();

        // 獲取鎖的時候,將當前線程的排隊號保存起來
        ticketLocal.set(currentTicketNum);
        while (currentTicketNum != queueNum.get()){
            // doSomething...
        }
    }

    // 釋放鎖:從排隊緩衝池中取
    public void unLock(){
        Integer currentTicket = ticketLocal.get();
        queueNum.compareAndSet(currentTicket,currentTicket + 1);
    }

}

此次就再也不須要返回值,辦業務的時候,要將當前的這一個號碼緩存起來,在辦完業務後,須要釋放緩存的這條票據。

缺點

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 ThreadLocal<CLHNode> LOCAL = new ThreadLocal<>();
    private static final AtomicReferenceFieldUpdater<CLHLock,CLHNode> UPDATER =
            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 是一種基於鏈表的可擴展、高性能、公平的自旋鎖,申請線程只在本地變量上自旋,直接前驅負責通知其結束自旋,從而極大地減小了沒必要要的處理器緩存同步的次數,下降了總線和內存的開銷。MCS 來自於其發明人名字的首字母: John Mellor-Crummey和Michael Scott。

public class MCSLock {

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

    private static final ThreadLocal<MCSNode> NODE = new ThreadLocal<>();

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

    private static final AtomicReferenceFieldUpdater<MCSLock,MCSNode> UPDATE =
            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多處理器緩存同步的問題。

總結

此篇文章咱們主要講述了自旋鎖的提出背景,自旋鎖是爲了提升資源的使用頻率而出現的一種鎖,自旋鎖說的是線程獲取鎖的時候,若是鎖被其餘線程持有,則當前線程將循環等待,直到獲取到鎖。

自旋鎖在等待期間不會睡眠或者釋放本身的線程。自旋鎖不適用於長時間持有CPU的狀況,這會加重系統的負擔,爲了解決這種狀況,須要設定自旋週期,那麼自旋週期的設定也是一門學問。

還提到了自旋鎖自己沒法保證公平性,那麼爲了保證公平性又引出了TicketLock ,TicketLock 是採用排隊叫號的機制來實現的一種公平鎖,可是它每次讀寫操做都必須在多個處理器緩存之間進行緩存同步,這會致使繁重的系統總線和內存的流量,大大下降系統總體的性能。

因此咱們又引出了CLHLock和MCSLock,CLHLock和MCSLock經過鏈表的方式避免了減小了處理器緩存同步,極大的提升了性能,區別在於CLHLock是經過輪詢其前驅節點的狀態,而MCS則是查看當前節點的鎖狀態。

文章參考:

https://blog.csdn.net/qq_34337272/article/details/81252853

http://www.blogjava.net/jinfeng_wang/archive/2016/12/14/432088.html

https://blog.hufeifei.cn/ 關於自旋鎖的文章

https://en.wikipedia.org/wiki/Ticket_lock

下面爲本身作個宣傳,歡迎關注公衆號 Java建設者,號主是Java技術棧,熱愛技術,喜歡閱讀,熱衷於分享和總結,但願能把每一篇好文章分享給成長道路上的你。 關注公衆號回覆 002 領取爲你特地準備的大禮包,你必定會喜歡並收藏的。

file

本文由博客一文多發平臺 OpenWrite 發佈!

相關文章
相關標籤/搜索