AQS實現原理
ReentrantLock和Synchronization一樣是可重入鎖,Synchronization是sun公司開發,而ReentrantLock是一個叫Doug Lea的人寫出來的。它控制鎖的狀態是通過AQS(抽象隊列同步器)來實現的,說白了就是 等待隊列 + CAS。
(1)aqs內部有一個被volatile修飾的int變量state,、默認是0,還有一個變量記錄當前加鎖線程、默認為null。
(2)線程1調用lock()方法后進行加鎖,由於還沒被別人獲取到鎖,所以state就成了1,並且記錄當前線程為線程1。其實每次線程一重入加鎖1次,會判斷當前加鎖線程就是自己,那么他自己就可以可重入多次加鎖,每次加鎖就是把 state 的值給累加 1,unlock()就減一,別的沒啥變化。
(3)此時線程二來加鎖,發現state 不等於0,然后去檢查是否自己獲取到了鎖,發先獲取鎖失敗,則進入AQS的等待隊列。直到鎖被釋放后,線程二就被喚醒。
源碼解讀
aqs是如何基於cas加鎖的
我們 new ReentrantLock(); 就是創建了一個 NonfairSync 它是Sync 抽象靜態內部類的實現,重寫了lock方法。這個 Sync 又繼承了 AbstractQueuedSynchronizer 類,也就是我們常說的 AQS 類。
我們執行 lock() 加鎖,實際就是調用的 Unsafe 的 CAS 方法,我在 atomic 源碼 里面已經分析過,就不再多說了。只是 atomic 是 while() 無限循環嘗試加鎖,AQS 加鎖一次,沒有搶到鎖就扔到隊列里面去了。
aqs如何基於state實現重入鎖的
上面如果 cas 加鎖成功,就會標記線程占用標識;那么此時又有一個線程進來加鎖呢?這時就走到了 acquire(1) 方法了。可以看到在 tryAcquire 里面首先是獲取到當前線程,然后獲取到此時 state 的值。由於已經被人修改過,那肯定是1,這里再 if(c==0) 只是保證代碼的健壯性。(你在外面 !=0 , 萬一現在已經釋放了呢)。
然后再 if (current == getExclusiveOwnerThread()) 判斷占用鎖的線程和自己是不是同一個線程,是的話就相加,nextc 就成了2然后賦值到 state 里。返回加鎖成功!
異步入隊阻塞等待
上面是同一個線程,進行重入。那如果是不同線程呢,肯定是獲取不到鎖的。此時就會先走 addWaiter() 方法,他會將自己封裝成一個 node 入隊,阻塞等待別人釋放鎖, 通過分析它的屬性看出它是一個 雙向鏈表 的隊列結構。addWaiter() 方法就是將當前請求排隊到隊列中去。
請求入隊后,現在就來到了 acquireQueued() ,它負責把當前線程掛起來,阻塞等待。首先又是一個無線for循環,獲取到 node 的上一個節點 p,如果 p 是頭節點,則說明當前 node 是排隊的第一個節點(因為上面看到了 head 其實就是 new 出來的一個空 node) ,那么 node 就會執行 tryAcquire() 又去嘗試獲取鎖。然后將自己從這個隊列中摘除掉;如果 p 不是頭節點或者 node 沒有搶到鎖,就執行了下面的 if 分支,shouldParkAfterFailedAcquire 獲取當前線程的 waitStatus 狀態,默認是 0 ,然后將它更新為 SIGNAL,然后走到下面通過調用 LockSupport.park(this); 將該線程阻塞等待、一直卡着不動, 等待被別人喚醒。所以我們接下來看看解鎖的邏輯
解鎖
我們加鎖是 state+1, 然后 標記線程占用標識。那么解鎖肯定是要去除線程標識、state-1的,只是還有線程掛在隊列中,得去從頭部叫醒才行。
tryRelease 先是 getState 獲取到的是1,因為被人加鎖了(如果重入了就大於1),然后 int c = 1-1; 此時把 0 setState,然后將線程設置為空。再執行 unparkSuccessor , 獲取到頭部node s , LockSupport.unpark() 喚醒它。阻塞的線程被喚醒后,又會再次執行它的 acquireQueued() 方法的 for 循環去搶鎖。
公平鎖
java 並發包很多鎖默認的策略都是非公平的,也就是可能后來的線程先加鎖,先來的線程后加鎖。比如剛才線程二在排隊時,線程一釋放鎖之后,可能被線程三搶先拿到鎖。通過設置ReentrantLock lock = new ReentrantLock(true)就成了公平鎖,他們會按照隊列先后順序獲取鎖。可以看到:
NonfairSync:他就是非公平鎖,每次 lock() 都會去 compareAndSetState() 搶一下鎖。
FairSync:公平鎖,每次 lock() 時,都會 !hasQueuedPredecessors() 判斷一下是否可以嘗試加鎖。主要就是看有沒有節點在排隊;如果有,則看看是否是當前線程。
lock() 與 tryLock()
沒啥好說的,就時間判斷而已;lock 拿不到鎖就 阻塞等待了;tryLock 到了時間返回 false 跳出循環而已。
ReentrantLock和Synchronization比較
ReentrantLock和synchronized在低並發的時候性能差距不大,高並發時ReentrantLock性能要稍微高一些。雖然sync做了優化但是在競爭激烈的時候還是會從偏向鎖升級為重量級鎖,是用戶態切換到內核態的一個過程 比較消耗資源,lock有利用CAS自旋操作來實現鎖則會稍微好一點。but !大量開源框架中,還是 sync 用的多。
。