java并發包下很多API都是基于AQS來實現的加鎖和釋放鎖等功能的,比如ReentrantLock、ReentrantReadWriteLock底層都是基于AQS來實現的。AQS是java并發包的基礎類,今天這一節主要是梳理AQS的重要概念和原理細節。
ReentrantLock核心組件
ReentrantLock核心有3個組件:state、owner、AQS(抽象隊列同步器,簡單的說就是一個等待隊列Queue)。如下圖所示:
ReentrantLock內部包含了一個三個靜態內部類,Sync,FairSync和NonfairSync,其中FairSync和NonfairSync都是繼承自AQS。
AQS中的維護了一個state變量,通過volatile修飾的int類型的,代表了加鎖的狀態。初始狀態下,這個state的值是0。
private volatile int state;
AQS中維護了head、tail以及Node內部類等,這里構成了一個FIFO(先進先出)的線程等待隊列。
private transient volatile Node head;
private transient volatile Node tail;
static final class Node {
//節點狀態
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile Node prev;
volatile Node next;
//節點持有的線程
volatile Thread thread;
//....省略其他...
}
同時AQS又繼承了AbstractOwnableSynchronizer類,因此又維護了一個owner變量,用來記錄當前加鎖的是哪個線程,初始化狀態下,這個變量是null。
private transient Thread exclusiveOwnerThread
AQS加鎖和釋放原理
1、加鎖流程
假設有兩個線程同時過來申請鎖資源,線程1先獲得鎖,線程2需要等待的話,主要的流程如下:
- 兩個線程同時調用 lock 方法,線程1通過
CAS(0,1)
操作將state值從0變為1,成功加鎖。 - 線程1通過
CAS(0,1)
成功后,設置當前加鎖線程為自己 - 線程2此時也進行
CAS(0,1)
,此時state已經為1調用肯定會失敗 - 于是再查看加鎖線程是否為自己,如果不是則放入等待隊列
- 線程2此時調用
LockSupport.park()
掛起當前線程。
2.釋放流程
- 線程1執行任務成功調用 unlock 方法,state變量的值遞減1,如果state值為0,加鎖線程設置為null,徹底釋放鎖。
- 此時進入等待隊列的隊頭,調用
LockSupport.unpark()
喚醒線程2重新嘗試加鎖 - 線程2CAS操作將state從0變為1成功之后代表加鎖成功,將state設置為1。
- 把“加鎖線程”設置為線程2自己,同時線程2自己就從等待隊列中出隊了。
公平與非公平,可重入與獨占
1.ReentrantLock是如何實現非公平和公平的?
ReentrantLock通過兩個Sync的子類——FairSync和NonfairSync,默認是非公平鎖的實現,先來看非公平的源碼:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
}
再看公平鎖的源碼,和上面相比僅僅是一個if
邏輯的區別。非公平的鎖NonfairSync會多了一個判斷,先嘗試來加個鎖。
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
}
非公平的這個if
操作可以理解為如果線程1釋放了,別人過來加鎖,直接先嘗試插個隊的意思,有可能AQS隊列中的線程2還沒被喚醒了,被別人搶走了鎖,讓別的線程加鎖成功了。可以概況為一句話講,由于非公平鎖,加鎖前多了一個嘗試獲取鎖的操作,導致了可能會有線程插隊。
另外在公平鎖的嘗試加鎖過程中,還多了一個 hasQueuedPredecessors()
的判斷,這里限制了如果有人排隊,其他線程就不能插隊加鎖。所以就算線程1釋放鎖,線程3過來加鎖,由于lock方法沒有了非公平鎖的if(上來嘗試CAS修改state,加鎖的代碼),線程3就只能入隊。
這里可以看到公平鎖比非公平鎖的代碼多了一個判斷,判斷隊列中是否有等待線程。有的話也只能乖乖排隊。
總結來看,公平鎖主要體現在兩點上:
- 第一處是在lock方法加鎖時,沒有嘗試CAS加鎖修改state的if判斷,保證釋放鎖的時候不會有線程插隊加鎖。(非公平鎖中,入隊過程可能還會嘗試加鎖,也有可能造成插隊加鎖,大家可以自己屢一下代碼看看)
- 第二處是如果已經有人加鎖,在入隊過程中,也通過一個hasQueuedPredecessors判斷保證了按順序入隊,在入隊的過程中不會再次嘗試加鎖。
2.可重入鎖和不可重入鎖
ReentrantLock通過AQS的state變量巧妙的實現了可重入加鎖。如果是同一個線程調用了lock方法,加鎖,state會在現有值上加+1,每再次加一次鎖,就是一次可重入,所以就加鎖可重入鎖。也就是說:同一個線程可以使用同一個ReentrantLock進行反復加鎖。(sychronized的重量級Monitor鎖也是可重入鎖。)
另外,釋放鎖的話,肯定需要釋放所多次,同一個線程加鎖了幾次,就需要釋放幾次,需要將state值恢復為0才算真正的釋放鎖,別的線程才能獲取到。
3. 獨占鎖 VS 共享鎖
所謂獨占鎖,就是只要有一個線程加鎖,其他人都得靠邊站,這把鎖屬于某個線程獨占,這就是獨占鎖。默認reentrantLock.lock創建的是非公平的可重入獨占鎖!
共享鎖是什么意思呢?意思就是可以和別的線程同時持有一把鎖,比如之后要講的讀寫鎖。線程1加了讀鎖,線程2還是可以加讀鎖的,它們共享一把鎖。這樣的鎖就是一把共享鎖。
參考引用:
1、初識ReenranctLock加鎖的AQS底層原理
2、你知道ReentrantLock 公平、非公平、可重入、獨占、共享鎖都是什么意思嗎?
3、我畫了35張圖,就是為了讓你深入理解 AQS
4、大白話聊聊Java并發面試問題之談談你對AQS的理解?【石杉的架構筆記】