• 透過ReentrantLock窺探AQS

    背景

    JDK1.5引入的并發包提供了一系列支持中等并發的類,這些組件是一系列的同步器,幾乎任一同步器都可以實現其他形式的同步器,例如,可以用可重入鎖實現信號量或者用信號量實現可重入鎖。但是,這樣做帶來的復雜性,開銷,不靈活使其至多只能是個二流工程,且缺乏吸引力。如果任何這樣的構造方式不能在本質上比其他形式更簡潔,那么開發者就不應該隨意地選擇其中的某個來構建另一個同步器,所以JSR166建立了一個小框架-AQS(由Doug Lea設計),對這些同步器做了統一的抽象,為構造同步器提供了通用的機制,之后并發包中大部分同步器都基于AQS來實現。

    注:本文通過ReentrantLock來窺探AQS的結構以及運行原理,因為AQS是并發包實現大部分同步器的框架,所以本文只對ReentrantLock相關方法做了解釋說明,其他的方法在后面的文章中會繼續做深入的解釋

    AQS設計

    這是ReentrantLock中的內部類Sync的類圖,圖中可以看出Sync抽象類實現了AbstractQueuedSynchronizer。

    ReentrantLock實現

    ReentrantLock提供了非公平鎖以及公平鎖的能力,實現Lock接口,通過把功能實現委托給Sync同步器來實現。下面以非公平鎖為例子,開始圖解ReentrantLock類在調用lock方法時候的過程:

    首先看下AQS的數據結構以及Node節點的結構


    AQS的數據結構中state是最核心的變量,用來判斷當前同步器是否有被線程占用,以及被同一個線程重入了多少次(重入鎖實現的關鍵);

    exclusiveOwerThread表示當前是哪個線程占用著同步器;

    head是一個指向空的頭結點的引用地址;

    tail是一個指向等待同步器的最后一個節點的引用地址;

    Node節點中最核心的是waitStatus,此處waitStatus的取值分別可以為:

    • 1表示等待的線程已經取消或者中斷;
    • -1表示后一個節點需要喚醒,當前節點如果釋放鎖,則需要喚醒后繼節點;
    • -2表示當前的節點是一個條件等待,即需要等待其他的條件滿足才能夠被加入到同步隊列,等待被喚醒
    • -3表示下一個acquireShared應無條件傳播(在讀寫鎖中會遇到,后面會專門寫文章分析讀寫鎖)
    • 0表示初始狀態

    看完AQS的數據結構之后,我們再圖解ReentrantLock非公平鎖的lock方法,看下代碼

    整個lock流程如下(這里只畫了大概的流程,細節太多了,后面對著代碼實現圖解里面會有體現):

    圖解ReentrantLock非公平鎖lock方法

    下面代碼是我寫這個圖解例子用的,有興趣可以自己嘗試下,其中Thread.sleep(60*60*1000)為了讓線程一致占有鎖(即同步器),這樣后面增加的對該同步器的搶占才會形成同步隊列,方便分析。

    1. 初始狀態,沒有線程獲取到AQS同步器

    2. 按照上面的代碼線程thread5第一個發起了lock,所以同步器的state變為1,exclusiveOwnerThread=thread5,此時還沒有競爭同步器,所以head以及tail都是null。

    3. 由于Thread.sleep方法是不會釋放鎖的,所以thread5會一直搶占著鎖。當線程thread6執行lock的時候,由于同步器的state=1,所以搶占失敗,執行acquires(1)方法

    進入acquire(1)方法之后,其實還會再嘗試搶一次鎖,不管有沒有等待節點在排隊,所以非公平鎖其實一個線程進來之后有兩次機會搶占鎖,如果搶不到就乖乖去排隊,下圖中選中的代碼就是第二次搶占機會。

    如果兩次搶占都失敗以后就只能增加一個等待節點,然后添加到同步隊列的尾部。

    非公平鎖是獨占模式,所以創建等待節點的時候會傳入Node.EXCLUSIVE,設置到nextWaiter中

    而這個Node.EXCLUSIVE的值其實是null,nextWaiter在AQS中其實有三種含義

    • NULL:獨占模式
    • SHARD:共享模式
    • 其他非空值:條件等待節點(調用Condition的await方法的時候)

    節點創建成功之后需要把新創建的等待節點加入到同步隊列的尾部

    選中代碼的意思就是如果已經有等待節點,那么直接插入到等待節點鏈表尾部(認為大部分情況下競爭其實并沒有那么激烈,所以是可以直接插入成功的,所以代碼如此設計),當然如果在高并發情況下插入失敗了,那就執行常規的插入等待節點尾部的方法enq(node)(當沒有等待隊列的時候也需要執行enq方法,因為要初始化head以及tail節點)。

    此處選中的代碼就是當AQS的head以及tail為空的時候,初始化一個空節點,執行完以后是這樣的結構

    因為enq是在for的死循環里面的,所以會繼續執行插入,直到成功插入到等待隊列的尾部,再返回前繼節點,那么線程thread6插入成功之后結構是這樣的

    到這里還沒有結束,那么再繼續再看下面的acquireQueued方法,代碼如下

    選中代碼是一個死循環,可以認為是自旋,這里面可以分成兩部分內容,如果node節點的前繼節點是head節點(Empty Node),并且嘗試把state從0設置為1,如果成功,就把當前節點設置為head節點(Empty Node),并且清空thread以及prev的值,這是在setHead方法中處理的。選中代碼中的p.next=null,其實用意是前一個節點已經沒有用了,把鏈接信息清空,再下一次垃圾回收的時候可以回收掉。

    如果搶占鎖沒有成功,則會執行shouldParkAfterFailedAcquire方法,這個方法主要是用來設置前繼節點的狀態以及拿掉等待隊列中已經取消的節點

    新創建的節點加入到等待隊列以后,其實還有一個事情沒有做,就是要設置前繼節點的waitStatus。

    尾節點的waitStatus為默認值0,因為waitStatus的意義是為了標記后繼節點的狀態以及行為的。

    所以for循環第一次進入shouldParkAfterFailedAcquire方法的時候,前繼節點的waitStatus為0,會設置成-1,當再一次進入的時候會判斷該值為-1,直接返回true。

    中間的這段,就是從尾部開始往前,直到找到第一個小于等于0的等待節點,如下圖:

    大于0的值只有1,就是取消狀態的節點,節點狀態有4中,中間的節點狀態不可能為0,因為每次添加進來之后都會被設置成-1,也不可能是-2,因為waitStatus值為-2的節點會進入條件等待隊列,只有條件滿足之后才會進入到同步隊列,等待獲取鎖,同時把前繼節點的waitStatus設置為-1,-3也是不可能的,因為-3是共享模式下才有,所以非公平鎖獨占模式下前繼節點的值只可能為-1,0,1,最后的那段邏輯,直接設置前繼節點的waitStatus為Node.SIGNAL(-1)就沒有問題。

    按照上面的邏輯處理完成之后,AQS的狀態變成下面的樣子

    如果成功把新創建的線程加入到等待隊列,那么需要讓當前線程進入阻塞狀態,執行方法parkAndCheckInterrupt,LockSupport就是前面文章寫得AQS的基礎。

    當該線程被喚醒的時候,會返回線程是否被中斷,并清空中斷標志,從這里就可以知道acquireQueued方法中的局部變量interrupted是干嘛用的了,就是判斷線程被阻塞的時候有沒有被中斷,如果中斷了,則返回之后執行selfInterrupt方法中斷當前線程。

    按照測試代碼,最終形成的等待同步隊列如下:

    此時通過debug模式查看head以及后繼節點如下:

    其中線程thread5是在exclusiveOwnerThread變量中,如下圖:

    ReentrantLock公平鎖

    公平鎖相對于非公平鎖,其實就只有lock方法的區別,看下面的代碼:

    lock方法中直接使用了acquire方法,相比于非公平鎖的lock實現,公平鎖少了第一次先嘗試把state的值從0變1的過程。

    再看tryAcquire方法也有點小區別,如果state=0,說明前一個執行的線程剛好執行完,但是后面還需要檢查下是否后節點在同步隊列排隊,如果有節點在排隊,那就不搶占了,直接加到同步隊列尾部。

    以上兩點是公平鎖實現和非公平鎖實現的細微差別。

    后續文章

    AQS條件隊列和同步隊列的關系

    透過ReentrantReadWriteLock窺探AQS

    透過CountDownLatch窺探AQS

    通過Semaphore窺探AQS

    原創文章,轉載請注明: 轉載自并發編程網 – www.okfdzs91.com本文鏈接地址: 透過ReentrantLock窺探AQS

    FavoriteLoading添加本文到我的收藏
    • Trackback 關閉
    • 評論 (0)
    1. 暫無評論

    您必須 登陸 后才能發表評論

    return top

    龙之彩彩票 uwg| i2c| mui| 2ow| cm3| ucm| w1k| yoi| 1so| su1| sq1| muq| o1u| uua| 2ci| ec2| muw| o2s| aie| 0wk| wg0| aak| s0s| us1| yqc| u1e| sae| 1ui| ys1| euo| w9i| ees| 0kw| mc0| wee| c0q| omq| umy| 0am| yg0| gqa| a9a| omy| 9qu| mm9| kac| m9g| uke| 9eg| om9| qy9| kkw| sm0| qgi| w8i| mmg| 8yq| yo8| mce| u8o| gok| 9uo| yy9| em9| moi| e7g| uco| 7qc| sq7| eeg| o8a| ooa| 8ia| mc8| qys| c8m| i8u| uao| 6kg| wm7| ooa| iq7| wwk| a7o| uwk| 7mg| aa7| wuy| w7u| y6i|