AQS
Lock的特性:
- 可重入
這個特性就是加了幾次鎖也要釋放幾次鎖
synchronized也有可重入性 - 公平性與非公平性
實現鎖的核心:
- CAS
保證加鎖永遠只有一個線程能夠成功 - LockSupport
對線程阻塞和喚醒 - 自旋
cas加鎖失敗,則這些線程就要自旋,阻塞住 就不佔用cpu資源 - queue
放阻塞的那些線程,用隊列是因爲隊列的FIFO可以保證公平性
CAS
CAS Compare and swap
能夠保證不管併發有多高,都能保證這個執行的原子性
通過cas算法去加鎖,這樣保證加鎖永遠只有一個線程能夠成功
CAS 工作原理
主內存中有一個expect=0,如果兩個線程都要去修改expect,則CAS就會讓這兩個線程都複製一份到自己線程中,然後再用另一個變量比如是refresh存修改後的值, 線程A和線程B就都去和主內存比較 如果expect的值相等,則將refresh中的值修改主內存的值,如果expect值不等,則不改主內存的值。
CAS的使用
Unsafe類中提供了三個關於CAS的方法:
線程阻塞就不會佔用cpu的資源
通過java的LockSupport.part() 就可以阻塞線程
等到線程Aunlock()的時候 喚醒線程B 這樣線程B就可以接着去執行 去加鎖
LockSupport.park和unpark的使用
ReentrantLock
ReentrantLock定義在java.util.concurrent包下
java.util.concurrent包中很多功能就是基於AQS框架的
公平鎖和非公平鎖實現的特性就是通過 在ReentrantLock內部定義了一個Sync的內部類,該類繼承AbstractQueuedSynchronized,對 該抽象類的部分方法做了實現;
並且還定義了兩個子類:
這兩個類都繼承自Sync,也就是間接繼承了AbstractQueuedSynchronized,所以這一個 ReentrantLock同時具備公平與非公平特性。
- FairSync 公平鎖的實現
- NonfairSync 非公平鎖的實現
AQS源碼分析
超類中有一個變量:記錄當前獲取鎖的線程是誰
AQS類下的變量state狀態器,表明當前同步器的狀態
state爲0表明是無鎖狀態,沒有被任何一個線程持有
隊列的創建:
AQS會基於Node構建一條隊列,Node是AQS的內部類,
Node的重要屬性:
- Node的prev和next屬性用來形成雙向鏈表。
- Node的thread屬性用來保持對線程的引用
- Node的SHARED屬性表示鎖是共享鎖,Semaphore鎖是共享的
- Node的EXCLUSIVE屬性表示鎖是互斥的,ReentrantLock需要鎖是互斥的
- Node的waitestate屬性表示當前結點的生命狀態(信號量)
- SIGNAL=-1 可被喚醒
- CANCELLED=1 代表出現異常,中斷引起的,需要廢棄結束
- CONDITION=-2 條件等待
- PROPAGATE=-3 傳播
- 0 是初始狀態Init狀態
AQS的head屬性會指向Node的頭部,tail屬性會指向Node的尾部
形成的雙向隊列:
公平鎖:
FairSync的lock()方法調用acquire()方法
acquire()方法:
-
tryAcquire 嘗試去獲取鎖:
- 通過Thread.currentThread()獲取當前線程的引用
- getState() 獲取同步器的狀態
- c==0 無鎖狀態
-
對於公平鎖,首先要判斷是否有線程在排隊
通過判斷隊列的隊頭隊尾是否一樣 -
沒有線程排隊,才用CAS去加鎖,加鎖其實就是把改同步器的狀態爲1
-
把當前線程的引用賦給exclusiveOwnerThread
-
- c!=0
- 第一種情況,這個鎖是被當前線程持有的,則再對state++
Lock的可重入性就是通過這部分邏輯做到的 - 第二種情況,這個鎖是被其他線程持有的,則返回false表示加鎖失敗
- 第一種情況,這個鎖是被當前線程持有的,則再對state++
- c==0 無鎖狀態
-
addWaiter 線程入隊
返回隊尾的node
-
創建Node結點
入參是當前線程引用和mode是EXCLUSIVE互斥鎖,這時默認waiteState即爲0 -
enq(node)
-
t==null 則給隊列初始化
構建隊列要先給隊列做初始化,即創建一個空結點,thread爲null,隊頭head隊尾tail同時指向這個創建好的空結點
-
t!=null 進行入隊操作
入隊也存在競爭,爲了保證所有阻塞線程對象能夠被喚醒即都能入隊,所以要用CAS保證入隊的原子性- 把prev指向t即隊尾指向的node
- 用CAS的方式移動尾部指針
- 原來尾部即t的node的next指向當前入隊的node
-
-
-
acquireQueued 阻塞
-
如果當前node是隊列的第一個 則再通過tryAcquire嘗試獲取鎖,儘可能避免線程被阻塞。
- 獲取到鎖了 節點就出隊。 並且把head往後挪一個節點
通過setHead 把head指向當前node,並把當前node的thread、pred置位null,也就是變成了一個空結點 - 如果沒有搶到鎖 就阻塞
第一輪循環,通過shouldParkAfterFailedAcquire修改head的狀態爲-1即SIGNAL
第二輪循環,阻塞線程。-
shouldParkAfterFailedAcquire
取出前驅結點的狀態waitStatus,當前結點能否被喚醒取決於前驅結點的狀態
- 如果前驅結點的狀態是signal ws==Node.SIGNAL 直接返回true代表是當前結點可喚醒的
- ws>0 代表前驅節點 出現異常要被cancelled
- ws是0或propagate,我們就通過CAS方式將前驅節點設置爲可喚醒狀態SIGNAL,即將ws設置爲-1.
head的waitState設置爲-1的原因:因爲持有鎖的線程T0在釋放鎖的時候,會去喚醒隊列中排隊的第一個線程T1,要判斷head的waitState是否!=0。成立的話會把waitState改爲0,然後把把T1被喚醒;T1接着走循環去搶鎖,可能會再失敗(在非公平鎖場景下),就會再次被阻塞,head的節點就又經歷兩輪循環 waitState從0又變成-1.
-
parkAndCheckInterrupt 阻塞線程,並且需要判斷線程是否是由中斷信號喚醒的
調用LockSupport.park進行阻塞
-
- 獲取到鎖了 節點就出隊。 並且把head往後挪一個節點
-
持有鎖的這邊邏輯
執行unlock()
release()
這裏把-1又改成0的原因是,如果在非公平鎖的情況下,當前線程被喚醒後有可能還是會搶不到鎖,那這樣就要保持是0的狀態繼續去執行阻塞的邏輯