前言
談到併發,我們不得不說AQS(AbstractQueuedSynchronizer)
,所謂的AQS
即是抽象的隊列式的同步器,內部定義了很多鎖相關的方法,例如:
- getState():獲取鎖的標誌state值
- setState():設置鎖的標誌state值
- tryAcquire(int):獨佔方式獲取鎖。嘗試獲取資源,成功則返回true,失敗則返回false。
- tryRelease(int):獨佔方式釋放鎖。嘗試釋放資源,成功則返回true,失敗則返回false。
這裏還有更多的方法並沒有列出來,我們以ReentrantLock
作爲突破點通過源碼和畫圖的形式一步步瞭解AQS
內部實現原理。
目錄結構
文章準備模擬場景來進行解析:
三個線程(線程一、線程二、線程三)同時來加鎖/釋放鎖,然後通過代碼和畫圖一步步解析其中的實現。
目錄如下:
- 線程一加鎖成功時
AQS
內部實現 - 線程二/三加鎖失敗時
AQS
中等待隊列的數據模型 - 線程一釋放鎖及線程二獲取鎖實現原理
這裏會分析每個線程加鎖、釋放鎖內部的一系列實現原理
AQS實現原理
AQS
中 維護了一個volatile int state
(代表共享資源)和一個FIFO
線程等待隊列(多線程爭用資源被阻塞時會進入此隊列)。
這裏volatile
能夠保證多線程下的可見性,當state=1
則代表當前對象鎖已經被佔有,其他線程來加鎖時則會失敗,然後線程放入一個FIFO
的等待隊列中使用UNSAFE.park()
來掛起當前線程。
另外state
的操作都是使用CAS
來保證其併發修改的安全性。
具體原理我們可以用一張圖來簡單概括:(此圖片來源:https://www.cnblogs.com/waterystone/p/4920797.html)
image.png
場景分析
線程一加鎖成功
如果同時有三個線程併發搶佔鎖,此時線程一搶佔鎖成功:
此時線程二、線程三加鎖失敗:
搶佔鎖代碼實現:
java.util.concurrent.locks.ReentrantLock .NonfairSync:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
這裏使用的ReentrantLock非公平鎖,線程進來直接利用CAS
嘗試搶佔鎖,如果搶佔成功則state
被改爲1,然後設置獨佔鎖線程爲當前線程。如下所示:
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
線程二搶佔鎖失敗
我們按照真實場景來分析,此時線程一搶佔鎖成功,state
變爲1,所以線程二通過CAS
修改state
變量必然會失敗。此時AQS
中FIFO
隊列中數據如圖所示:
我們將線程二執行的邏輯一步步拆解來看:
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire()
:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
先看看tryAcquire()
的具體實現:java.util.concurrent.locks.ReentrantLock .nonfairTryAcquire()
:
final boolean nonfairTryAcquire(int acquires) {
// 獲取當前線程
final Thread current = Thread.currentThread();
// 獲取state狀態,線程一加鎖成功了,此時state=1
int c = getState();
// 如果state=0,說明可以嘗試利用CAS進行加鎖操作
if (c == 0) {
// 加鎖成功的邏輯
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果當前線程和獨佔鎖線程是同一個,那麼可以重入加鎖
// 重入加鎖後,state=2
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 返回false加鎖失敗
return false;
}
這段代碼走下來,此時全局變量state
=1,所以通過CAS
修改state
的值不會成功。
而此時持有鎖的線程是線程一,所以線程二*也不滿足重入的條件。
線程二執行tryAcquire()
後返回false,接着執行addWaiter(Node.EXCLUSIVE)
,代碼實現如下:
java.util.concurrent.locks.AbstractQueuedSynchronizer.addWaiter()
:
private Node addWaiter(Node mode) {
// mode = Node EXCLUSIVE = null
// 創建一個新的node,thread = 線程二,nextWaiter = null
Node node = new Node(Thread.currentThread(), mode);
// 此時tail = null,直接執行enq操作
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 線程二直接進入隊列操作
enq(node);
return node;
}
此時tail
指針爲空,直接調用enq(node)
將當前線程加入等待隊列尾部:
private Node enq(final Node node) {
for (;;) {
// 第一次循環tail = null
Node t = tail;
if (t == null) {
// 用CAS將head設置爲一個新Node
if (compareAndSetHead(new Node()))
// tail 和 head都指向這個新Node
tail = head;
} else {
// 第二次循環進入,node的前置節點設置爲tail = head
node.prev = t;
// 用CAS操作設置tail節點爲當前傳入的node節點
if (compareAndSetTail(t, node)) {
// t開始時指向tail=head節點,通過CAS將tail指向node後
// 設置t.next=head.next=node
t.next = node;
// 返回t=head節點
return t;
}
}
}
}
第一遍循環tail
指針爲空,進入if邏輯中,此時隊列中數據:
執行完成之後,head
、tail
、t
都指向第一個元素(new Node()
)
接着執行第二遍循環,進入else邏輯,此時已經有了head節點,這裏要操作的就是將線程二這個Node節點掛到head
節點上來。
addWaiter()
方法執行完後會返回當前插入線程二構建的Node節點,此時隊列中的數據爲:
再接着看看acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
執行邏輯,此時傳入的爲線程二構建的Node
信息:
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued()
:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// p爲線程二Node的前置節點,也就是head節點
final Node p = node.predecessor();
// p= head成立,繼續使用CAS嘗試加鎖,此時線程一還在持有鎖
// state = 1,所以加鎖失敗
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 執行shouldParkAfterFailedAcquire方法
// shouldParkAfterFailedAcquire方法中將head節點的waitStatus變爲了SIGNAL=-1
// 接着執行parkAndChecknIterrupt,調用LockSupport.park()
// 掛起當前線程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndChecknIterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// ws爲head節點的waitStatus=null
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 通過CAS操作,將head節點的waitStatus變爲SIGNAL=-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
// 調用底層的park方法,掛起當前線程
LockSupport.park(this);
return Thread.interrupted();
}
acquireQueued()
這個方法會先判斷當前傳入的Node
對應的前置節點是否爲head
,如果是則嘗試加鎖。加鎖成功過則將當前節點設置爲head
節點,然後空置之前的head
節點。
如果加鎖失敗或者Node
的前置節點不是head
節點,首先將Node
的前置節點中的waitStatus
設置爲SIGNAL
(值爲-1),
然後掛起當前Node
節點(當前Node
爲線程二創建的節點),操作後AQS隊列中的數據如下圖:
此時線程二就靜靜的待在AQS
的等待隊列裏面了,等着其他線程釋放鎖來喚醒掛起的線程。
線程三搶佔鎖失敗
看完了線程二搶佔鎖失敗的分析,那麼再來分析線程三搶佔鎖失敗就很簡單了,先看看addWaiter(Node mode)
方法:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// pred = tail = 線程二
Node pred = tail;
// 此時pred不爲空
if (pred != null) {
// 設置線程三的前置節點爲線程二
node.prev = pred;
// 使用CAS將線程三設置爲tail節點
if (compareAndSetTail(pred, node)) {
// pred = 線程二的next節點設置爲線程三
pred.next = node;
// 返回線程三節點
return node;
}
}
enq(node);
return node;
}
執行完後AQS
中隊列數據如圖:
接着執行acquireQueued()
方法:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// p = 線程三的前置節點= 線程二
final Node p = node.predecessor();
// 判斷線程二是否是head節點,如果是則嘗試搶佔鎖
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 執行shouldParkAfterFailedAcquire方法
// 將線程三的前置節點線程二中的waitStatus變爲SIGNAL = -1
// 然後執行parkAndCheckInterrupt將線程三也掛起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
執行完後AQS
中隊列數據如圖:
線程一釋放鎖
現在來分析下釋放鎖的過程,首先是線程一釋放鎖,釋放鎖後會喚醒head
節點的後置節點,也就是我們現在的線程二,執行完後AQS
隊列數據如下:
此時線程二已經被喚醒,繼續嘗試獲取鎖,如果獲取鎖失敗,則會繼續被掛起。如果獲取鎖成功,則AQS
中數據如圖:
接着還是一步步拆解來看,先看看線程一釋放鎖的代碼:
java.util.concurrent.locks.AbstractQueuedSynchronizer.release()
public final boolean release(int arg) {
// tryRelease() 方法實現在ReentrantLock中實現的
if (tryRelease(arg)) {
// 如果釋放鎖成功,定義h=head節點
Node h = head;
// 如果head不爲空,此時head.waitStatus=SIGNAL=-1
if (h != null && h.waitStatus != 0)
// 執行喚醒操作,喚醒之前掛起的線程
unparkSuccessor(h);
return true;
}
return false;
}
此時看ReentrantLock.tryRelease()
中的具體實現:
protected final boolean tryRelease(int releases) {
// getState()是獲取state變量,此時state=1
// releases傳入進來的也是1,所以c=0
int c = getState() - releases;
// 如果釋放鎖的線程,不是當前獨佔鎖線程,直接拋異常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果c=0則說明釋放鎖成功
if (c == 0) {
free = true;
// 設置獨佔鎖線程爲null
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
執行完ReentrantLock.tryRelease()
後,state
被設置成0,Lock對象的獨佔鎖被設置爲null。可以看下執行後AQS中的數據:
接着執行java.util.concurrent.locks.AbstractQueuedSynchronizer.unparkSuccessor()
方法,喚醒head
的後置節點:
private void unparkSuccessor(Node node) {
// node爲head線程,此時node.waitStatus=SIGNAL=-1
int ws = node.waitStatus;
if (ws < 0)
// 設置node節點的waitStatus爲0
compareAndSetWaitStatus(node, ws, 0);
// s=node.next=head.next,獲取線程二Node節點
Node s = node.next;
// 此時s.waitStatus=SIGNAL=-1 條件不成立
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 執行LockSupport.unpark,這裏是喚醒線程二
if (s != null)
LockSupport.unpark(s.thread);
}
邏輯如圖:
此時線程二被喚醒,線程二接着之前被park
的地方繼續執行,繼續執行acquireQueued()
方法。
線程二喚醒繼續加鎖
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 獲取線程二的前置節點爲head
final Node p = node.predecessor();
// p=head,然後嘗試加鎖,對state進行CAS操作
// 此時仍有可能加鎖失敗,因爲我們使用的ReentrantLock中的非公平鎖
// 如果真好有新的線程進來掙錢鎖,線程二就有可能加鎖失敗
// 如果加鎖失敗就還會被掛起
if (p == head && tryAcquire(arg)) {
// 線程二加鎖成功,將線程二設置爲head節點
setHead(node);
// 設置p.next=head.next爲null,方便GC
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
執行完後線程二獲取鎖,而線程二也變爲head
節點,之前的head
節點被空置,等着被垃圾回收,此時AQS
中隊列數據如下:
此時 線程二獲取鎖成功,並且之前的head
節點會被垃圾回收掉。
線程二釋放鎖/線程三加鎖
當線程二釋放鎖時,會喚醒被掛起的線程三,流程和上面大致相同,被喚醒的線程三會再次嘗試加鎖,具體代碼就不再分析了,此時AQS
中隊列數據如圖:
總結
這裏用了一步一圖的方式來展示了ReentrantLock
的實現方式,而ReentrantLock
底層就是基於AQS
實現的,所以我們也對AQS
有了深刻的理解。
由於篇幅原因,還有很多細節沒有講到,比如ReentrantLock
公平鎖的實現,可重入鎖的實現,Condition
的實現等,當然大家可以依照着我這種分析模式去嘗試一步步畫圖解析,相信這些細節也都能一步步破解。
後面我還會介紹ReentrantReadWriteLock
的實現原理,仍然使用一步一圖的模式來講解,敬請期待。