1. 程式人生 > >深入理解Lock的底層實現原理

深入理解Lock的底層實現原理

lock的實現完全是由java寫的,和作業系統或者是JVM虛擬機器沒有任何關係。整體來看Lock主要是通過兩個東西來實現的分別是CAS和ASQ(AbstractQueuedSynchronizer)。通過加鎖和解鎖的過程來分析鎖的實現。

加鎖

一、整體概述流程

1. 讀取表示鎖狀態的變數

2. 如果表示狀態的變數的值為0,那麼當前執行緒嘗試將變數值設定為1(通過CAS操作完成),當多個執行緒同時將表示狀態的變數值由0設定成1時,僅一個執行緒能成功,其它執行緒都會失敗。失敗後進入佇列自旋轉並阻塞當前執行緒。

     2.1 若成功,表示獲取了鎖,

            2.1.1 如果該執行緒(或者說節點)已位於在佇列中,則將其出列(並將下一個節點則變成了佇列的頭節點)

           2.1.2 如果該執行緒未入列,則不用對佇列進行維護

            2.1.3 然後當前執行緒從lock方法中返回,對共享資源進行訪問。

     2.2 若失敗,則當前執行緒將自身放入等待(鎖的)佇列中並阻塞自身此時執行緒一直被阻塞在lock方法中,沒有從該方法中返回(被喚醒後仍然在lock方法中,並從下一條語句繼續執行,這裡又會回到第1步重新開始)

3. 如果表示狀態的變數的值為1,那麼將當前執行緒放入等待佇列中,然後將自身阻塞

         注意: 喚醒並不表示執行緒能立刻執行,而是表示執行緒處於就緒狀態,僅僅是可以執行而已

二、具體的實現細節(非公平鎖)

      簡單說來,AbstractQueuedSynchronizer會把所有的請求執行緒構成一個CLH佇列,當一個執行緒執行完畢(lock.unlock())時會啟用自己的後繼節點,但正在執行的執行緒並不在佇列中,而那些等待執行的執行緒全部處於阻塞狀態,經過調查執行緒的顯式阻塞是通過呼叫LockSupport.park()完成,而LockSupport.park()則呼叫sun.misc.Unsafe.park()本地方法,再進一步,HotSpot在Linux中中通過呼叫pthread_mutex_lock函式把執行緒交給系統核心進行阻塞。

1、lock方法實現(公平鎖與非公平鎖的由來)

     當有執行緒競爭鎖時,當前執行緒會首先嚐試獲得鎖而不是在佇列中進行排隊等候,這對於那些已經在佇列中排隊的執行緒來說顯得不公平,這也是非公平鎖的由來。原始碼如下:
 final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
對於剛來競爭的執行緒首先會通過CAS設定狀態,如果設定成功那麼直接獲取鎖,執行臨界區的程式碼,反之呼叫acquire(1)進入同步佇列中。如果已經存在Running執行緒,那麼CAS肯定會失敗,則新的競爭執行緒會通過CAS的方式被追加到隊尾。

2、解析acquire(1)方法

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
當CAS同步狀態為1失敗時才會執行上面的程式碼,上面的程式碼的作用是完成同步狀態的獲取,構造用於放入佇列中的節點(可以理解為執行緒任務),加入到佇列中,單個節點自己自旋用於檢查目前佇列中的狀況以及當前節點或者是執行緒阻塞。該方法主要由以下幾個方法構成 tryAcquire() addWaiter()和AcquireQueued()。

 2.1 nonfairTryAcquire 獲取同步狀態

    
final boolean nonfairTryAcquire(int acquires) {  
    final Thread current = Thread.currentThread();  
    int c = getState();  
    if (c == 0) {  
        if (compareAndSetState(0, acquires)) {  
            setExclusiveOwnerThread(current);  
            return true;  
        }  
    }  
    else if (current == getExclusiveOwnerThread()) {//說明有執行緒擁有了該鎖,這個執行緒就是自己本身那麼狀態++
        int nextc = c + acquires;  
        if (nextc < 0) // overflow  
            throw new Error("Maximum lock count exceeded");  
        setState(nextc);  
        return true;  
    }  
    return false;  
}  

1、 該方法會首先判斷當前狀態,如果c==0說明沒有執行緒正在競爭該鎖,如果不c !=0 說明有執行緒正擁有了該鎖。  2、 如果發現c==0,則通過CAS設定該狀態值為acquires,acquires的初始呼叫值為1,每次執行緒重入該鎖都會+1,每次unlock都               會-1,但為0時釋放鎖,這也就是為什麼一個lock要對應這個一個unlock的原因 3、如果CAS設定成功,則可以預計其他任何執行緒呼叫CAS都不會再成功,也就認為當前執行緒得到了該鎖,也作為Running執行緒,很       顯然這個Running執行緒並未進入等待佇列。 4、如果c !=0 但發現自己已經擁有鎖,只是簡單地++acquires,並修改status值,但因為沒有競爭,所以通過setStatus修改,而非        CAS,也就是說這段程式碼實現了偏向鎖的功能。

2.2  addWaiter 構建入隊節點

private Node addWaiter(Node mode) {  
    Node node = new Node(Thread.currentThread(), mode);  
    // Try the fast path of enq; backup to full enq on failure  
    Node pred = tail;  
    if (pred != null) {  
        node.prev = pred;  
        if (compareAndSetTail(pred, node)) {  
            pred.next = node;  
            return node;  
        }  
    }  
    enq(node);  
    return node;  
} 
addWaiter方法負責把當前無法獲得鎖的執行緒包裝為一個Node新增到隊尾。
其中引數mode是獨佔鎖還是共享鎖,預設為null,獨佔鎖。追加到隊尾的動作分兩步: 
     1、如果當前隊尾已經存在(tail!=null),則使用CAS把當前執行緒更新為Tail 
      2、如果當前Tail為null或則執行緒呼叫CAS設定隊尾失敗,則通過enq方法繼續設定Tail 
  下面是enq方法:
private Node enq(final Node node) {  
    for (;;) {  
        Node t = tail;  
        if (t == null) { // Must initialize  
            Node h = new Node(); // Dummy header  
            h.next = node;  
            node.prev = h;  
            if (compareAndSetHead(h)) {  
                tail = node;  
                return h;  
            }  
        }  
        else {  
            node.prev = t;  
            if (compareAndSetTail(t, node)) {  
                t.next = node;  
                return t;  
            }  
        }  
    }  
}  
該方法就是迴圈呼叫CAS,即使有高併發的場景,無限迴圈將會最終成功把當前執行緒追加到隊尾(或設定隊頭)。總而言之,addWaiter的目的就是通過CAS把當前執行緒追加到隊尾,並返回包裝後的Node例項。

2.3 acquireQueued 執行緒對外行為上阻塞,內部自旋

final boolean acquireQueued(final Node node, int arg) {  
    try {  
        boolean interrupted = false;  
        for (;;) {  
            final Node p = node.predecessor();  
            if (p == head && tryAcquire(arg)) {//前驅節點等於頭節點,嘗試獲取同步狀態
                setHead(node);  
                p.next = null; // help GC  
                return interrupted;  
            }  
            if (shouldParkAfterFailedAcquire(p, node) &&  
                parkAndCheckInterrupt())  
                interrupted = true;  
        }  
    } catch (RuntimeException ex) {  
        cancelAcquire(node);  
        throw ex;  
    }  
} 
      acquireQueued的主要作用是把已經追加到佇列的執行緒節點(addWaiter方法返回值)進行阻塞,但阻塞前又通過tryAccquire重試是否能獲得鎖,如果重試成功能則無需阻塞,直接返回。
     仔細看看這個方法是個無限迴圈,感覺如果p == head && tryAcquire(arg)條件不滿足迴圈將永遠無法結束,當然不會出現死迴圈,奧祕在於第12行的parkAndCheckInterrupt會把當前執行緒掛起,從而阻塞住執行緒的呼叫棧。
private final boolean parkAndCheckInterrupt() {  
    LockSupport.park(this);  
    return Thread.interrupted();  
}  
如前面所述,LockSupport.park最終把執行緒交給系統(Linux)核心進行阻塞。當然也不是馬上把請求不到鎖的執行緒進行阻塞,還要檢查該執行緒的狀態,比如如果該執行緒處於Cancel狀態則沒有必要,具體的檢查在shouldParkAfterFailedAcquire中, shouldParkAfterFailedAcquire就是靠前繼節點判斷當前執行緒是否應該被阻塞,如果前繼節點處於CANCELLED狀態,則順便刪除這些節點重新構造佇列。 

解鎖

      請求鎖不成功的執行緒會被掛起在acquireQueued方法的第12行,12行以後的程式碼必須等執行緒被解鎖鎖才能執行,假如被阻塞的執行緒得到解鎖,則執行第13行,即設定interrupted = true,之後又進入無限迴圈。
   從無限迴圈的程式碼可以看出,並不是得到釋放鎖的執行緒一定能獲得鎖,必須在第6行中呼叫tryAccquire重新競爭,因為鎖是非公平的,有可能被新加入的執行緒獲得,從而導致剛被喚醒的執行緒再次被阻塞,這個細節充分體現了“非公平”的精髓。通過之後將要介紹的解鎖機制會可以發現,第一個釋放的執行緒就是Head,因此p == head的判斷基本都會成功。
    解鎖程式碼相對簡單,主要體現在AbstractQueuedSynchronizer.release和Sync.tryRelease方法中class AbstractQueuedSynchronizer
public final boolean release(int arg) {  
    if (tryRelease(arg)) {  
        Node h = head;  
        if (h != null && h.waitStatus != 0)  
            unparkSuccessor(h);  
        return true;  
    }  
    return false;  
}  

protected final boolean tryRelease(int releases) {  
    int c = getState() - releases;  
    if (Thread.currentThread() != getExclusiveOwnerThread())  
        throw new IllegalMonitorStateException();  
    boolean free = false;  
    if (c == 0) {  
        free = true;  
        setExclusiveOwnerThread(null);  
    }  
    setState(c);  
    return free;  
}  
tryRelease語義很明確:如果執行緒多次鎖定,則進行多次釋放,直至status==0則真正釋放鎖,所謂釋放鎖即設定status為0,因為無競爭所以沒有使用CAS。 release的語義在於:如果可以釋放鎖,則喚醒佇列第一個執行緒(Head),具體喚醒程式碼如下:
private void unparkSuccessor(Node node) {  
    /* 
     * If status is negative (i.e., possibly needing signal) try 
     * to clear in anticipation of signalling. It is OK if this 
     * fails or if status is changed by waiting thread. 
     */  
    int ws = node.waitStatus;  
    if (ws < 0)  
        compareAndSetWaitStatus(node, ws, 0);   

    /* 
     * Thread to unpark is held in successor, which is normally 
     * just the next node.  But if cancelled or apparently null, 
     * traverse backwards from tail to find the actual 
     * non-cancelled successor. 
     */  
    Node s = node.next;  
    if (s == null || s.waitStatus > 0) {  
        s = null;  
        for (Node t = tail; t != null && t != node; t = t.prev)  
            if (t.waitStatus <= 0)  
                s = t;  
    }  
    if (s != null)  
        LockSupport.unpark(s.thread);  
}
這段程式碼的意思在於找出第一個可以unpark的執行緒,一般說來head.next == head,Head就是第一個執行緒。以上就是加鎖解鎖的全部過程,需要注意幾點: 1、正在執行的執行緒節點並沒有在佇列中 2、首節點被喚醒後只是說不是阻塞狀態了,並不是說他一定可以執行,還需要嘗試獲取同步狀態來競爭是執行還是再次被阻塞(命運多舛啊)。

簡述總結:

    總體來講執行緒獲取鎖要經歷以下過程(非公平):     1、呼叫lock方法,會先進行cas操作看下可否設定同步狀態1成功,如果成功執行臨界區程式碼     2、如果不成功獲取同步狀態,如果狀態是0那麼cas設定為1.     3、如果同步狀態既不是0也不是自身執行緒持有會把當前執行緒構造成一個節點。     4、把當前執行緒節點CAS的方式放入佇列中,行為上執行緒阻塞,內部自旋獲取狀態。     5、執行緒釋放鎖,喚醒佇列第一個節點,參與競爭。重複上述。

相關推薦

深入理解Git的實現原理

原文地址:https://www.cnblogs.com/mamingqian/p/9711975.html 0、導讀   本文適合對git有過接觸,但知其然不知其所以然的小夥伴,也適合想要學習git的初學者,通過這篇文章,能讓大家對git有豁然開朗的感覺。在寫作過程中,我力求

深入理解 MySQL 底層實現

MySQL 的常用引擎 1. InnoDB InnoDB 的儲存檔案有兩個,字尾名分別是 .frm 和 .idb,其中 .frm 是表的定義檔案,而 idb 是資料檔案。 InnoDB 中存在表鎖和行鎖,不過行鎖是在命中索引的情況下才會起作用。 InnoDB 支援事務,且支援四種隔離

深入理解 Dijkstra 演算法實現原理

迪傑斯特拉(Dijkstra)演算法 1典型最短路徑演算法,用於計算一個節點到其他節點的最短路徑。 2特點是以起始點為中心向外層層擴充套件(廣度優先搜尋思想),直到擴充套件到終點為止。 大概就是這樣一個有權圖,Dijkstra演算法可以計算任意節點到其他節點的最短

深入解析Vue底層實現原理

本次給大家整理Vue底層實現原理的知識點總結,寫的十分的全面細緻,具有一定的參考價值,對此有需要的朋友可以參考學習下。如有不足之處,歡迎批評指正。 前言 最近在研究 剖析Vue原理&實現雙向繫結MVVM 這篇文章,一邊學習一邊總結一下自己的思考。 Vue是一個典型的MVV

深入解析ThreadLocal底層實現原理

學習Java中常用的開源框架,Mybatis、Hibernate中設計到執行緒通過資料庫連線物件Connection,對其資料進行操作,都會使用ThreadLocal類來保證Java多執行緒程式訪問和資料庫資料的一致性問題。就想深入瞭解一下ThreadLocal類是怎樣確保執

深入理解Lock底層實現原理

lock的實現完全是由java寫的,和作業系統或者是JVM虛擬機器沒有任何關係。整體來看Lock主要是通過兩個東西來實現的分別是CAS和ASQ(AbstractQueuedSynchronizer)。

深入理解 Tomcat(三)Tomcat 底層實現原理

轉載自:https://blog.csdn.net/qq_38182963/article/details/78660777 又是一個週末,這篇文章將從一個簡單的例子來理解tomcat的底層設計; 本文將介紹 Java Web 伺服器是如何執行的, W

深入理解spring事務底層實現原理

事務 相信大家都在ATM機取過錢,但是是否有人考慮過它的流程是怎樣的呢? 我們都知道,假如我們取300塊錢,那麼當這三百塊錢從ATM機出來時,我們的賬戶相應的會減少300。這兩個過程一定是要同時成功才算成功的。否則就會出現賬戶少了300.但是錢沒出來,對於我們來

iOS底層原理總結-- 深入理解 KVC\KVO 實現機制

iOS底層原理總結–OC物件的本質(一) - 掘金 iOS底層原理總結–OC物件的本質(二) - 掘金 iOS底層原理總結–OC物件的分類:instance、class、meta-calss物件的isa和superclass - 掘金 iOS底層原理總結-- KVO/KVC的本質

java併發機制的底層實現原理(一):volatile深入分析

     java程式碼最終會被類載入器載入到JVM中,然後轉化為彙編指令在CPU上執行。java中所使用的併發機制依賴於JVM的實現和CPU的指令。 1.volatile的應用 volatile是一個輕量級的synchronize,它保證了共享變數的可見性,確保了所有執

Syschronized的底層實現原理以及各種鎖的理解

      java中每個物件都可作為鎖,鎖有四種級別,按照量級從輕到重分為:無鎖、偏向鎖、輕量級鎖、重量級鎖。每個物件一開始都是無鎖的,隨著執行緒間爭奪鎖,越激烈,鎖的級別越高,並且鎖只能升級不能降級。 java物件頭  鎖的實現機制與java物件頭息息相關,鎖的所有資

深入理解ArrayList集合內部原理並自主封裝程式碼實現ArrayList集合功能

集合框架是java基礎學習中非常重要的一部分,學會集合用法的同時去了解一下集合內部程式碼實現原理對我們日後的java學習的幫助是十分大的;我們現在來了解一下ArrayList原理:ArrayList內部其實就是封裝一個預設固定大小的物件陣列;不過陣列的大小是可動

一文帶你理解Java中Lock實現原理

當多個執行緒需要訪問某個公共資源的時候,我們知道需要通過加鎖來保證資源的訪問不會出問題。java提供了兩種方式來加鎖,一種是關鍵字:synchronized,一種是concurrent包下的lock鎖。synchronized是java底層支援的,而concurrent包

深入分析Volatile的實現原理

queue 鏈接地址 什麽 高速緩存 spa 其中 帶來 系統內存 單詞 引言 在多線程並發編程中synchronized和Volatile都扮演著重要的角色,Volatile是輕量級的synchronized,它在多處理器開發中保證了共享變量的“可見性”。可見性的意思是當

iOS分類底層實現原理小記

tag side 遍歷 一個 instance ati strip 否則 取出 http://www.jianshu.com/p/b7169a5a558e OS 分類底層是怎麽實現的?本文將分如下四個模塊進行探究 分類的結構體 編譯時的分類 分類的加載 總結 本文使用

Java中HashMap底層實現原理(JDK1.8)源碼分析

blank imp dash logs || 屬性 lte das ces 這幾天學習了HashMap的底層實現,但是發現好幾個版本的,代碼不一,而且看了Android包的HashMap和JDK中的HashMap的也不是一樣,原來他們沒有指定JDK版本,很多文章都是舊版本J

SDWebImage底層實現原理

key 失敗 一些事 imp finish 最好 緩存機制 交互 取圖 SDWebImage底層實現有沙盒緩存機制,主要由三塊組成 1、內存圖片緩存2、內存操作緩存3、磁盤沙盒緩存內部實現過程:第一步,下載SDWebImage,導入工程。 第二步,在需要的地方導入頭文件

Java並發機制和底層實現原理

差距 32處理器 們的 trac 結點 exce jdk cep 定性   Java代碼在編譯後會變成Java字節碼,字節碼被類加載器加載到JVM裏,JVM執行字節碼轉化為匯編指令在CPU上執行。Java中的並發機制依賴於JVM的實現和CPU的指令。      Java語言

HashMap底層實現原理

cati 是我 次數 max turn 索引 線程安全 出現 獲取 一、數據結構 HashMap中的數據結構是數組+單鏈表的組合,以鍵值對(key-value)的形式存儲元素的,通過put()和get()方法儲存和獲取對象。 (方塊表示Entry對象,橫排表示數組t

深入分析synchronized的實現原理

test 代碼塊 mage this rgs 需要 pub 釋放 javap 基礎概念   synchronized可以保證方法或者代碼塊在運行時,同一時刻只有一個方法可以進入到臨界區,同時可以保證共享變量對內存可見性。   Java中每一個對象都可以作為鎖,這是syn