1. 程式人生 > >AbstractQueuedSynchronizer原始碼剖析(六)- 深刻解析與模擬執行緒競爭資源

AbstractQueuedSynchronizer原始碼剖析(六)- 深刻解析與模擬執行緒競爭資源

1、細說AQS

在深入分析AQS之前,我想先從AQS的功能上說明下AQS,站在使用者的角度,AQS的功能可以分為兩類:獨佔鎖和共享鎖。它的所有子類中,要麼實現並使用了它獨佔鎖的API,要麼使用了共享鎖的API,而不會同時使用兩套API,即便是它最有名的子類ReentrantReadWriteLock,也是通過兩個內部類:讀鎖和寫鎖,分別實現的兩套API來實現的,到目前為止,我們只需要明白AQS在功能上有獨佔鎖和共享鎖兩種功能即可。

2、ReentrantLock的呼叫過程



AbstractQueuedSynchronizer中抽象了絕大多數Lock的功能,而只把tryAcquire方法延遲到子類中去實現。tryAcquire方法的語義在於用具體子類判斷請求執行緒是否可以獲得鎖,無論成功與否AbstractQueuedSynchronizer都將處理後面的流程。

3、獲取鎖的過程

簡單說來,AbstractQueuedSynchronizer會把所有的請求執行緒構成一個CLH佇列,當一個執行緒執行完畢(lock.unlock())時會啟用自己的後繼節點,但正在執行的執行緒並不在佇列中,而那些等待執行的執行緒全部處於阻塞狀態。

4、非公平鎖的加鎖流程

(1). 首先我們分析非公平鎖的的請求過程。我們假設在這個時候,還沒有任務執行緒獲取鎖,這個時候,第一個執行緒過來了(我們使用的是非公平鎖),那麼第一個執行緒thread1會去獲取鎖,這時它會呼叫下面的方法,通過CAS的操作,將當前AQS的state由0變成1,證明當前thread1已經獲取到鎖,並且將AQS的exclusiveOwnerThread設定成thread1,證明當前持有鎖的執行緒是thread1。:
/**
 * Performs lock.  Try immediate barge, backing up to normal
 * acquire on failure.
 */
final void lock() {
    // 如果鎖沒有被任何執行緒鎖定且加鎖成功則設定當前執行緒為鎖的擁有者
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

(2). 此時來了第二個執行緒thread2,並且我們假設thread1還沒有釋放鎖,因為我們使用的是非公平鎖,那麼thread2首先會進行搶佔式的去獲取鎖,呼叫NonFairSync.lock方法獲取鎖。NonFairSync.lock方法的第一個分支是通過CAS操作獲取鎖,很明顯,這一步肯定會失敗,因為此時thread1還沒有釋放鎖。那麼thread2將會走NonFairSync.lock方法的第二個分支,進行acquire(1)操作。acquire(1)其實是AQS的方法,acquire(1)方法內部首先呼叫tryAcquire方法,ReentrantLock.NonFairLock重寫了tryAcquire方法,並且ReentrantLock.NonFairLock的tryAcquire方法又呼叫了ReentrantLock.

Sync的nonfairTryAcquire方法,nonfairTryAcquire方法如下:

 /**
         * Performs non-fair tryLock.  tryAcquire is
         * implemented in subclasses, but both need nonfair
         * try for trylock method.
         */
        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. 獲取當前將要去獲取鎖的執行緒,在此時的情況下,也就是我們的thread2執行緒。

2. 獲取當前AQS的state的值。如果此時state的值是0,那麼我們就通過CAS操作獲取鎖,然後設定AQS的exclusiveOwnerThread為thread2。很明顯,在當前的這個執行情況下,state的值是1不是0,因為我們的thread1還沒有釋放鎖。

3. 如果當前將要去獲取鎖的執行緒等於此時AQS的exclusiveOwnerThread的執行緒,則此時將state的值加1,很明顯這是重入鎖的實現方式。在此時的執行狀態下,將要去獲取鎖的執行緒不是thread1,也就是說這一步不成立。

4. 以上操作都不成立的話,我們直接返回false。

既然返回了false,那麼之後就會呼叫addWaiter方法,這個方法負責把當前無法獲取鎖的執行緒包裝為一個Node新增到隊尾。通過下面的程式碼片段我們就知道呼叫邏輯:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

我們進入到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內部:

第一步:將當前將要去獲取鎖的執行緒也就是thread2和獨佔模式封裝為一個node物件。並且我們也知道在當前的執行環境下,執行緒阻塞佇列是空的,因為thread1獲取了鎖,thread2也是剛剛來請求鎖,所以執行緒阻塞佇列裡面是空的。很明顯,這個時候佇列的尾部tail節點也是null,那麼將直接進入到enq方法。

第二步:我們首先看下enq方法的內部實現。首先內部是一個自懸迴圈。

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

第一次迴圈:t為null,隨後我們new出了一個空的node節點,並且通過CAS操作設定了執行緒的阻塞佇列的head節點就是我們剛才new出來的那個空的node節點,其實這是一個“假節點”,那麼什麼是假節點”呢?那就是節點中不包含執行緒。設定完head節點後,同時又將head節點賦值給尾部tail節點,到此第一次迴圈結束。此時的節點就是如下:


第二次迴圈:

現在判斷尾部tail已經不是null了,那麼就走第二個分支了。將尾部tail節點賦值給我們傳遞進來的節點Node的前驅節點,此時的結構如下:


然後再通過CAS的操作,將我們傳遞進來的節點node設定成尾部tail節點,並且將我們的node節點賦值給原來的老的那個尾部節點的後繼節點,此時的結構如下:


這個時候程式碼中使用了return關鍵字,也就是證明我們經過了2次迴圈跳出了這個自懸迴圈體系。

按照程式碼的執行流程,接下來將會呼叫acquireQueued方法,主要是判斷當前節點的前驅節點是不是head節點,如果是的話,就再去嘗試獲取鎖,如果不是,就掛起當前執行緒。這裡可能有人疑問了,為什麼判斷當前節點的前驅節點是head節點的話就去嘗試獲取鎖呢?因為我們知道head節點是一個假節點,如果當前的節點的前驅節點是頭節點即是假節點的話,那麼這個假節點的後繼節點就有可能有獲取鎖的機會,所以我們需要去嘗試。

現在我們看下acquireQueued方法內部,我們也可以清楚的看到,這個方法的內部也是一個自懸迴圈。:

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

第一次迴圈:獲取我們傳入node的前驅節點,判斷是否是head節點,現在我們的狀態是:


很明顯滿足當前node節點的前驅節點是head節點,那麼現在我們就要去呼叫tryAcquire方法,也就是NonfairSync類的tryAcquire方法,而這個方法又呼叫了ReentrantLock.Sync.nonfairTryAcquire方法。

很明顯此時thread2獲取鎖是失敗的,直接返回false。按照呼叫流程,現在進入了當前節點的前驅節點的shouldParkAfterFailedAcquire方法,檢查當前節點的前驅節點的waitstatus。shouldParkAfterFailedAcquire方法內部如下:

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

1. 如果前驅節點的waitStatus為-1,也就是SIGNAL,就返回true。

2. 如果當前節點的前驅節點的waitstatus大於0,也就是說被CANCEL掉了,這個時候我們會除掉這個節點。

3. 如果都不是以上的情況,就通過CAS操作將這個前驅節點設定成SIGHNAL。

很明顯,我們在這裡的情況是第3種情況,並且這個方法執行後返回false。

此時的結構如下,主要是head節點的waitStatus由0變成了-1。


第二次迴圈:獲取我們傳入node的前驅節點,判斷是否是head節點,現在我們的狀態是:


很明顯滿足當前node節點的前驅節點是head節點,那麼現在我們就要去呼叫tryAcquire方法,也就是NonfairSync類的tryAcquire方法,而這個方法又呼叫了ReentrantLock.Sync.nonfairTryAcquire方法。

很明顯此時thread2獲取鎖是失敗的,直接返回false。按照呼叫流程,現在進入了當前節點的前驅節點的shouldParkAfterFailedAcquire方法,檢查當前節點的前驅節點的waitstatus。此時waitstatus為-1,這個方法返回true。shouldParkAfterFailedAcquire返回true後,就會呼叫parkAndCheckInterrupt方法,直接將當前執行緒thread2阻塞。仔細看這個方法acquireQueued方法,是無限迴圈,感覺如果p == head && tryAcquire(arg)條件不滿足迴圈將永遠無法結束,在這裡,當然不會出現死迴圈。因為parkAndCheckInterrupt會把當前執行緒阻塞。分析到這裡,我們的thread2執行緒已經被阻塞了,這個執行緒不會再繼續執行下去了。

(3). 假設現在我們的thread1還沒有釋放鎖,而現在又來了一個執行緒thread3。

thread3首先呼叫lock方法獲取鎖,首先去搶佔鎖,因為我們知道thread1還沒有釋放鎖,這個時候thread3肯定搶佔失敗,於是又呼叫了acquire方法,接著又失敗。接著會去呼叫addWaiter方法,將當前執行緒thread3封裝成node加入到執行緒阻塞佇列的尾部。現在的結構如下:


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;
    }

第一步:將當前將要去獲取鎖的執行緒也就是thread3和獨佔模式封裝為一個node物件。並且我們也知道在當前的執行環境下,執行緒阻塞佇列不是空的,因為thread2獲取了鎖,thread2已經加入了佇列。很明顯,這個時候佇列的尾部tail節點也不是null,那麼將直接進入到if分支。將尾部tail節點賦值給我們傳入的node節點的前驅節點。如下:


第二步:通過CAS將我們傳遞進來的node節點設定成tail節點,並且將新tail節點設定成老tail節點的後繼節點。


到此,addWaiter方法執行完畢,接著執行acquireQueued方法。這是一個自迴圈方法。

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
第一次迴圈:獲取我們傳入node的前驅節點,判斷是否是head節點,現在我們的狀態是:

我們傳入node的前驅節點不是head節點,那麼直接走第二個if分支,呼叫shouldParkAfterFailedAcquire方法。

1. 如果前驅節點的waitStatus為-1,也就是SIGNAL,就返回true。

2. 如果當前節點的前驅節點的waitstatus大於0,也就是說被CANCEL掉了,這個時候我們會除掉這個節點。

3. 如果都不是以上的情況,就通過CAS操作將這個前驅節點設定成SIGHNAL。

很明顯,我們在這裡的情況是第3種情況,並且這個方法執行後返回false。

此時的結構如下,主要是t節點的waitStatus由0變成了-1。

相關推薦

AbstractQueuedSynchronizer原始碼剖析- 深刻解析模擬執行競爭資源

1、細說AQS 在深入分析AQS之前,我想先從AQS的功能上說明下AQS,站在使用者的角度,AQS的功能可以分為兩類:獨佔鎖和共享鎖。它的所有子類中,要麼實現並使用了它獨佔鎖的API,要麼使用了共享鎖的API,而不會同時使用兩套API,即便是它最有名的子類Reentr

GTest原始碼剖析——RUN_ALL_TESTS

GTest原始碼剖析——RUN_ALL_TESTS 1 RUN_ALL_TESTS()原始碼分析 RUN_ALL_TESTS()之前是巨集定義,現在改變為函式。其實際上僅僅是呼叫了UnitTest單例的Run()函式。 原始碼如下: 其實現為獲

darknet原始碼剖析

繼續分析load_data_detection,進入fill_truth_detection函式。fill_truth_detection的作用是讀取圖片對應的標註資訊。 首先進入find_replace函式。 void find_replace(char *str, c

spring原始碼剖析AOP實現原理剖析

Spring的AOP實現原理,醞釀了一些日子,寫部落格之前信心不是很足,所以重新閱讀了一邊AOP的實現核心程式碼,而且又從網上找了一些Spring Aop剖析的例子,但是發現掛羊頭買狗肉的太多,標題高大上,內容卻大部分都是比較淺顯的一些介紹,可能也是由於比較少人閱讀這

Spark SQL原始碼剖析SQL解析框架Catalyst流程概述

Spark SQL模組,主要就是處理跟SQL解析相關的一些內容,說得更通俗點就是怎麼把一個SQL語句解析成Dataframe或者說RDD的任務。以Spark 2.4.3為例,Spark SQL這個大模組分為三個子模組,如下圖所示 其中Catalyst可以說是Spark內部專門用來解析SQL的一個框架,在H

Windows執行使用事件機制解決執行同步問題

事件相關函式: 1.建立事件:CreateEvent HANDLE CreateEvent ( LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState,

C++網路程式設計實戰專案--Sinetlib網路庫3——事件迴圈執行呼叫

上一篇文章講了Reactor模式的關鍵結構I/O複用和事件分發,現在我們來關注一下它們的使用。 事件迴圈 我們已經實現了一個Epoller類來實現I/O複用,具體的使用方法就是Epoller::Poll()函式等待事件的發生,該函式有一個超時時間,超過這個時間即

spring4.2.9 java專案環境下ioc原始碼分析——refresh之obtainFreshBeanFactory方法@4預設標籤bean,beans解析、最終註冊

接上篇文章,解析了import和alias標籤,只是開胃菜比較簡單,下面介紹bean標籤的載入,也是預設名稱空間下解析的重點。protected void processBeanDefinition(Element ele, BeanDefinitionParserDeleg

SpringMVC原始碼剖析- HandlerExceptionResolver異常解析器家族揭祕

在Spring MVC中,所有用於處理在請求處理過程中丟擲的異常,都要實現HandlerExceptionResolver介面。HandlerExceptionResolver是Spring MVC提供的非常好的通用異常處理工具,不過需要注意的是,它只能處理請求過程中丟擲的

spring原始碼剖析Spring預設標籤解析及註冊實現

在使用spring的時候,我也經常會使用到bean標籤,beans標籤,import標籤,aop標籤等。 下面主要為讀者介紹spring的預設的自帶標籤的解析流程。 驗證模式(DTD&XSD) dtd基本已被淘汰,現在spring的驗證模式基本都是採用xsd檔案

Flume NG原始碼分析應用程式使用的RpcClient設計

上一篇Flume NG原始碼分析(五)使用ThriftSource通過RPC方式收集日誌 介紹了ThriftSource利用Thrfit服務ThriftSourceProtocol來收集日誌。這篇說說flume-ng-sdk中提供給應用層序使用的RpcClient的設計和實現。繼續使用ThriftR

YOLOv2原始碼分析

文章全部YOLOv2原始碼分析 我們再次回到了parse_network_cfg函式 //parse_network_cfg else if(lt == ACTIVE){

Dubbo原始碼分析Dubbo通訊的編碼解碼機制

Dubbo原始碼分析(一)Dubbo的擴充套件點機制 Dubbo原始碼分析(二)Dubbo服務釋出Export Dubbo原始碼分析(三)Dubbo的服務引用Refer Dubbo原始碼分析(四)Dubbo呼叫鏈-消費端(叢集容錯機制) Dubbo原始碼分析(五)Dubbo呼叫鏈-服務端

## Zookeeper原始碼閱讀 Watcher

前言 好久沒有更新部落格了,最近這段時間過得很壓抑,終於開始踏上為換工作準備的正軌了,工作又真的很忙而且很瑣碎,讓自己有點煩惱,希望能早點結束這種狀態。 繼上次分析了ZK的ACL相關程式碼後,ZK裡非常重要的另一個特性就是Watcher機制了。其實在我看來,就ZK的使用而言,Watche機制是最核心的特性

Redis原始碼閱讀叢集-故障遷移(下)

Redis原始碼閱讀(六)叢集-故障遷移(下)   最近私人的事情比較多,沒有抽出時間來整理部落格。書接上文,上一篇裡總結了Redis故障遷移的幾個關鍵點,以及Redis中故障檢測的實現。本篇主要介紹叢集檢測到某主節點下線後,是如何選舉新的主節點的。注意到Redis叢集是無中心的,那麼使用分散式一

STL原始碼剖析迭代器traits程式設計

文章目錄 1. 迭代器概念 1.1 基本概念 1.2 迭代器設計理念 2. 引出traits程式設計 3. traits程式設計 3.1 traits程式設計技術 3.2 partial special

STL原始碼剖析空間配置器

歡迎大家來訪二笙的小房子,一同學習分享生活! 文章目錄 1. 寫在前面 2. SGI空間配置器 2.1 SGI標準空間配置器 2.2 SGI特殊的空間配置器,std::alloc 2.3 構造和析構基本工具 2.4 空間

STL原始碼剖析【hash_set、hash_map】

hash_set 與set區別及聯絡 與set大同小異,set以RB-tree作為其底層,而hash_set以hash table作為其底層 兩者都是藉由其底層操作完成我們所看到的那些操作 二者最大的不同在於set的底層RB-tree有自動排序功能,所以反映在se

STL原始碼剖析hashtable

文章目錄 1. hashtable概述 1.1 線性探測 1.2 二次探測 1.3 開鏈法 2. hashtable的桶與節點 3. hashtable迭代器 4. hashtable資料結構 5. has

STL原始碼剖析關聯式容器--【set、multiset、map、multimap】

文章目錄 1. 寫在前面 2. set 2.1 set性質 2.2 set實現 2.3 multiset 3. map 3.1 map性質 3.2 pair 3.3 map實現