jdk1.5開始增加了一種新的鎖,原有的鎖是通過對象頭中的一個鎖標識符來實現的,這里給大家分享一篇文章,http://www.lxweimin.com/p/c5058b6fe8e5。
這里的實現我們且看且分析,先來看看類關系結構圖,直接實現了Lock。
內部持有了一個Sync對象,根據描述可知,這個對象才是真正提供鎖實現機制的核心。
這個對象繼承自AbstractQueuedSynchronizer,子類支持公平和非公平兩種模式,使用的是AQS狀態去描述鎖的持有數量。AQS是并發包下一個非常重要的類,之后會專門寫一篇來講解。
默認使用的是非公平鎖,可以通過傳入布爾值true來使用公平鎖。
當我們調用lock命令的時候,其實真正調用的是sync對象的lock。
如果是非公平鎖,我們來看看實現,首先調用一個CAS將一個狀態從0變為1
這個方法是AQS里的一個方法,調用底層的UNSAFE的CASI更新狀態,那么這個stateOffset是什么呢?
從AQS的靜態初始化里可以看到這個是一個代表狀態的內存偏移量,說直白點就是表示一種狀態的值。
當更新成功后就進入了setExclusiveOwnerThread方法并傳入了當前線程。然后這個方法也是AQS里面實現的,在AQS里面直接賦值給一個從名字分析來看叫唯一的線程的變量。
如果在上一步修改失敗了進入acquire方法,這個方法里首先調用了一個treAcquire方法,并把傳入的參數傳進去,
TryAcquire方法在AQS里直接拋出了不支持操作的異常,這里調用的呢實際是nonFairSync里面重寫的方法,這里又直接調用了nonfairTryAcquire方法,
在這個方法里首先調用了getState方法獲得一個狀態變量,直接調用了AQS里的方法,返回一個代表鎖定次數的值。如果返回狀態為0,則說明當前無線程鎖定,則繼續調用前面的CAS去修改狀態為1,修改成功則設置當前線程為排他的主線程,然后返回true,或者返回false,也就是獲取鎖失敗,如果狀態不為0,則判斷當前線程與排他的主線程是否相等,則改變加鎖次數,如果這個次數小于0則拋出錯誤,然后調用setState方法把這個鎖次數設置為狀態,從這里可以理解為是一個可重入鎖的實現過程
在前面如果調用tryAcquire失敗后接著會調用acquireQueued方法,第一個參數里面先調用了一個addWaiter方法,并傳入了一個代表排他的node節點,,這里面先對線程和這個代表著線程阻塞模式的狀態包裝為node,然后先拿到node節點的尾節點,如果這個尾節點不為null,則把這個為節點設置為新建立的node的前節點形成一個鏈條,然后通過CAS把尾節點替換為新建的node,然后把前面節點的下一個節點指向新建節點,然后返回。如果為節點為null或者替換失敗則調用enq方法
enq方法里面調用一個線程循環,首先拿到尾節點,如果為尾節點為空,則先建立一個空的null節點并設置為Head,然后把尾節點也指向head,這相當于對node鏈的一個初始化,然后再次進入for循環。這時候頭尾節點不為Null了,或者如果不為null,則把傳入的新建node的前節點指向尾節點,然后通過CAS替換傳入的node為尾節點,最后把之前的尾節點的next指向新傳入的節點就完成了鏈條,這里其實仍然是一次重試node節點鏈條的建立,最后總是返回新建的節點。
寫到這里我們看到這里利用node構建了一個鏈條,雖然目前不知道這個鏈條的作用,但是我們可以猜測到這個鏈條應該是用于阻塞等待的。在這個方法里面首先初始化了兩個布爾值,一個用于判斷獲取結果是成功還是失敗,一個用于判斷線程狀態。然后進入一個for循環。通過調用新傳入的node.predecessor()方法獲取這個Node的前一個node,如果前一個是整個鏈條的head,線程的阻塞等待是通過這個頭節點的狀態為-1實現的,然后就會再次嘗試調用TraAcquire方法去獲取,如果獲取成功,則說明線程獲得了鎖,之后把獲得鎖的node(它持有當前線程)設置為頭node,然后持有的線程設置為null,,然后把node的前一個node置為null,獲取是否失敗的標志變為false,然后返回是否中斷的狀態。如果前面的判斷失敗則調用一個shouldParkAfterFailedAcquire方法,從名字分析是一個獲取失敗后暫存線程的操作。真正的線程阻塞在parkAnCheckInterrupt里面調用locksupport.park方法實現的。
這個方法里第一個參數是新建Node的前一個Node,第二個參數是我們新建的node,他的主要目的是判斷或者找到當前某一個node的等待狀態為-1,這里如果node大于0,說明已經持有鎖了,數值越大,說明重入次數越多,如果為0則說明在競爭鎖。首先判斷前一個node的線程狀態是什么,如果是signal(-1)狀態,則直接返回true,就可以安全的寄存線程了
如果不是則首先遍歷node鏈條找到狀態是0的節點,然后把我們新加進來的node變為這個節點的下一個節點,然后更新這個0的節點狀態為-1.
在這個方法里如果返回true,則執行parkAndCheckInterrupt()方法:這里首先調用LockSupport的park方法把線程寄存,然后在判斷線程的狀態,使用interrupted和interrupt方法的區別還記得嗎?如果調用了interrupted方法,會取消線程的中斷狀態。如果成功,則線程安全的寄存,如果寄存失敗,則返回線程的中斷狀態并取消這個中斷狀態。
到這里就完成了線程的等待,這里的核心是調用了park方法實現的。
下面來看下unlock的實現。在這里調用了AQS的release ?
在release方法里首先調用了一個tryRelease的方法,如果這個返回true,則先拿到頭節點,然后判斷頭節點不為null且他的等待狀態不為0,則調用unparkSuccessor方法。我們先看tryRelease方法
tryRelease(),首先對該線程的狀態進行一個線程鎖次數減操作,然后判斷當前線程是不是被設置為排他的線程,如果不是就拋出異常,如果是則先初始化一個是否成功的布爾值,如果此時鎖定次數為0,則直接設置這個布爾值為true,同時設置排他線程為null,最后調用setState改變這個線程的狀態,最后返回狀態,這里的狀態有2種,一種是鎖定次數變為0代表解鎖成功,如果不為0則代表重入了多次,需要多次調用來釋放。
返回true后我們看看unparkSuccessor()方法里面的實現,進來后首先拿到這個線程的等待狀態,如果小于0,先變更為0
然后拿到這個節點的下一個節點,判斷如果下一個節點不為Null且等待狀態為-1或者0,直接調用LockSupport.unpark()方法,這里注意傳入的對象是下一個節點,此時我們使用的是非公平鎖,那么這個下一個節點s是由怎么決定的呢?之后我們會詳細描述整個鏈條的構造過程。這里看如果等待狀態>0呢,首先把s設置為null,然后從整個鏈條的尾部開始遍歷,找到一個線程node的等待狀態為0或者-1的然后把這個節點,然后釋放這個節點。這里的意思是如果解鎖失敗的話會直接去尾部開始找一個等待的線程node去釋放。
由這個方法里面我們可以推斷出,解鎖的順序是按照獲得鎖的順序進行操作的。測試代碼如下:
上面代碼通過休眠100毫秒保證我們現場順序入鎖隊列。經過多次運行打印順序按照入隊順序。
入隊順序是通過addWaiter方法來實現整個鏈條的構建的。頭是一個線程為null的節點,狀態為-1,然后下一個指向第一個去搶鎖的線程Node,依次用next指向下一個。
tryLock();方法顧名思義是嘗試獲取一個鎖。這里調用了nonfairTryAcquire方法,前面已經介紹過了,如果獲取失敗則返回false。也就是獲取失敗。
tryLock還有一個帶時間參數的,底層調用的是一個帶參數的tryAcquireNanos方法,第一個是Long,第二個是代表時間單位。
這個方法里首先判斷線程是否被中斷,如果中斷拋出異常,如果沒中斷則取調用tryAcquire去獲取鎖,如果獲取成功直接返回,如果獲取失敗則調用doAcquireNanos方法,
doAcquireNanos方法里首先拿到排他的這個node,然后在for循環里去嘗試獲取鎖
如果失敗則調用parkNanos方法指定阻塞一定時間,然后繼續進入循環,知道我們的等待時間為0,然后獲取失敗去調用cancelAcquire方法,
這個方法里首先判斷這個node狀態,如果已經為Null則直接返回。如果不為null,則先把node的線程設置為null,然后沿著線程的前指針尋找直到一個等待狀態大于0的一個node,
然后把當前取消的節點和獲取到鎖的這個節點建立管理關系,然后將取消的節點狀態設置為-1,然后看這時候是不是尾節點,如果是則把前面的那個節點設置為尾節點,然后把前面節點的下一個節點設置為Null。如果不是尾節點先判斷是不是頭節點,如果不是則判斷這個Node狀態是不是-1,如果不是則繼續判斷是不是0或者是-1 如果滿足然后把這個節點狀態先更新為-1,且這個節點的前置節點不為null,然后取到我們要取消的節點的下一個節點,如果下一個節點不為null則把前面節點和下一個節點直接建立關系。如果上面條件不滿足則直接對要取消的節點執行解除寄存的操作,最后把要取消的節點的下一個節點指向自己,這樣就不存在引用問題可以回收了。
到此為止我們等待阻塞獲取也就看完啦。
lockInterruptibly方法之前在阻塞隊列里見到了很多個這個嘗試獲取鎖的方式,下面來看看他的實現,他也是調用了sync的acquireInterruptibly方法,
這里也會通過TryAcquire方法去嘗試獲取鎖,如果獲取成功直接返回,如果獲取失敗則調用doAcquireInterruptibly方法,
這個方法里首先把當前線程包裝成一個排他的的Node節點,然后去嘗試獲取鎖,這里與前面多線程競爭鎖唯一不一致的地方就是當調用parkAndCheckInterrupt方法成功后直接拋出了異常。而前面的操作是如果調用這個成功后,則把interrupted是否中斷的狀態改為true返回。
這里我們需要理解,調用Thread.interrupted(),如果線程被中斷則返回true,且釋放中斷,如果未被中斷則返回false, ?所以這種方式獲取鎖必須保證Thread未被中斷才能獲取到鎖。如果Thread執行過中斷,則獲取鎖失敗。
還剩最后一個newCondition方法,這里就不列代碼了,它總是調用對應的sync對象取獲取一個condition。
至此我們新的鎖的實現都可以很直觀的明了了。這就是一個線程競爭入隊的過程
這里補充一下,公平鎖,就是很公平,在并發環境中,每個線程在獲取鎖時會先查看此鎖維護的等待隊列,如果為空,或者當前線程線程是等待隊列的第一個,就占有鎖,否則就會加入到等待隊列中,以后會按照FIFO的規則從隊列中取到自己
非公平鎖比較粗魯,上來就直接嘗試占有鎖,如果嘗試失敗,就再采用類似公平鎖那種方式。
下面一篇會看公平鎖。