深入理解java同步、鎖機制我們主要講解了關於synchronized的實現和各種鎖的類型,本節將嘗試從源碼的角度去理解可重入鎖ReentrantLock的實現。由於個人水平有限,文中出現錯誤的地方還請指出,避免誤導更多人。
要理解ReentrantLock需要先理解所有鎖的基礎。AQS(AbstractQueuedSynchronizer)主要利用硬件原語指令(CAS compare-and-swap),來實現輕量級多線程同步機制,並且不會引起CPU上文切換和調度,同時提供內存可見性和原子化更新保證(線程安全的三要素:原子性、可見性、順序性)。AQS的本質上是一個同步器/阻塞鎖的基礎框架,其作用主要是提供加鎖、釋放鎖,並在內部維護一個FIFO等待隊列,用於存儲由於鎖競爭而阻塞的線程。AQS提供了兩種類型的鎖,一種是共享鎖一種是獨佔鎖。
AQS內部基於一個等待隊列(雙向)實現:
+------+ prev +-----+ +-----+
head | | <---- | | <---- | | tail
+------+ +-----+ +-----+
等待隊列是一個CLH隊列,並且它的頭節點時虛擬節點。AQS中有個非常重要的成員變量private volatile int state;用它來標示當前鎖的狀態,比如對於非可重入鎖來說0代表該鎖空閒,1代表該鎖已經被鎖定。
下面是ReentrantLock最簡單的使用方法:
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
//在需要同步的地方先加上鎖
lock.lock(); // block until condition holds
try {
//需要被同步語句
// ... method body
} finally {
//和sync塊不同的是需要手動釋放鎖。並且在finally中
lock.unlock()
}
}
}
雖然加鎖和解鎖只有非常簡單的兩句話,但是這背後的實現確實非常複雜的,下面來慢慢探究其中的實現細節。
首先從整體看下類的結構:
ReentrantLock類中有三個內部類,Sync是另外兩個類的父類,ReentrantLock的公平鎖和非公平鎖的實現就是通過Sync的兩個子類NonfairSync和FairSync來完成的。默認ReentrantLock實現的是非公平鎖,非公平鎖雖然失去了公平但是獲得了更好地吞吐量。
首先來看下ReentrantLock默認的lock方法也就是非公平鎖的lock方法:
final void lock() {
// 如果鎖沒有被任何線程鎖定且加鎖成功則設定當前線程爲鎖的擁有者
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 否則執行acquire方法傳入參數爲1
acquire(1);
}
關鍵的邏輯在acquire也就是當有線程發生競爭的時候:
//首先調用tryAcquire方法來再一次嘗試獲取鎖,如果成功則返回,否則執行acquireQueued方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//非公平鎖的獲取方法
final boolean nonfairTryAcquire(int acquires) {
//首先獲取當前線程
final Thread current = Thread.currentThread();
//獲取鎖的狀態
int c = getState();
//如果爲0則繼續通過原子操作設置state,如果成功則設置獲取鎖的線程爲當前線程並返回成功
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//否則鎖已經被某個線程獲取到,判斷是否爲當前線程
else if (current == getExclusiveOwnerThread()) {
//如果是當前線程則將state+1,可以看出鎖的可重入性就體現在這裏
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//設置狀態,並返回成功
setState(nextc);
return true;
}
//否則該鎖已經被其他線程佔用,因此後面需要考慮阻塞該線程。
return false;
}
//新增等待線程節點
private Node addWaiter(Node mode) {
//使用當前線程構造出新的節點
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//第一次tail肯定爲空則走enq(node)入隊列
Node pred = tail;
if (pred != null) {
//非第一次競爭
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
//入隊列操作
private Node enq(final Node node) {
//樂觀等待
for (;;) {
//第一次tail仍然爲空
Node t = tail;
if (t == null) { // Must initialize
//當第一次產生競爭的時候初始化虛擬頭結點,節省空間
Node h = new Node(); // Dummy header
//頭結點h的下一個節點指向前面新建的節點
h.next = node;
//雙向鏈表
node.prev = h;
//原子操作設置頭結點,如果成功則尾節點爲前面新建的節點,否則循環直到成功,如果同一時刻有其他線程set成功,則可能走else分支
if (compareAndSetHead(h)) {
//返回頭結點
tail = node;
return h;
}
}
else {
//否則
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
下面用圖來表示下入隊的操作,假設這是第一次發生競爭操作,且當前線程搶到了設置頭節點的權限則如下圖:
時刻要注意頭結點始終是虛擬節點,而此時new node也就是包含當前線程的節點是tail節點。實際上當header被設置好後,後面的入隊從隊尾進行,只有隊尾有多線程的競爭,類似頭部競爭,整個操作在else中。如上圖此時阻塞住一個線程,如果阻塞住兩個線程,則整個隊列類似下面:
注意在addWaiter方法中如果不是第一次競爭,直接設置tail,不進入enq方法,入隊操作成功後接下來是acquireQueued方法。
final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) {
// 如果當前線程是head的直接後繼則嘗試獲取鎖
// 這裏不會和等待隊列中其它線程發生競爭,但會和嘗試獲取鎖且尚未進入等待隊列的線程發生競爭。這是非公平鎖和公平鎖的一個重要區別
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
//如果當前線程獲取到鎖,這將頭結點設置爲當前節點,當前節點下一個節點設置爲null
setHead(node);
p.next = null; // help GC
//返回中斷狀態false
return interrupted;
}
//如果當前線程未獲取到鎖或者不是頭結點則進行下一步處理
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} catch (RuntimeException ex) {
cancelAcquire(node);
throw ex;
}
}
看下關鍵的shouldPark...方法
/**
* Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal
* control in all acquire loops. Requires that pred == node.prev
*
* @param pred node's predecessor holding status
* @param node the node
* @return {@code true} if thread should block
*/
//如果獲取鎖失敗,判斷是否應該掛起當前線程,可以預見掛起線程是有條件的
//這裏需要先說明一下waitStatus,它是AbstractQueuedSynchronizer的靜態內部類Node的成員變量,
//用於記錄Node對應的線程等待狀態.等待狀態在剛進入隊列時都是0,如果等待被取消則被設爲Node.CANCELLED,
//若線程釋放鎖時需要喚醒等待隊列裏的其它線程則被置爲Node.SIGNAL,還有一種狀態Node.CONDITION這裏先不討論。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//waitStatus初始值爲0,假設node前驅爲頭結點,但是沒有獲取到鎖,則進入else中
int ws = pred.waitStatus;
//如果狀態爲-1則表示當前線程可以被安全掛起
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park
*/
return true;
//如果大於0表示前一個節點處於被取消的狀態,直接剔除該狀態的節點
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 {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
//修改pred的狀態爲SINGAL -1,且不掛起當前線程
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
如果判斷可以park當前線程則調用parkAndCheckInterrupt()方法。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //阻塞,即掛起;在沒有unpark之前,下面的代碼將不會執行;
return Thread.interrupted();//Thread.interrupted()只是設置中斷狀態,當前線程被中斷過則返回true否則返回false
}
上面是lock的操作,有lock必然有unlock操作,下面再來看一下unlock的邏輯。
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
/*head的waitStatus不爲零表示它的後繼在等待喚醒,
還記得AbstractQueuedSynchronizer.shouldParkAfterFailedAcquire中對waitStatus的操作麼?
waitStatus!=0表明或者處於CANCEL狀態,或者是置SIGNAL表示下一個線程在等待其喚醒,CANCEL狀態這裏先不分析,
可以認爲這裏!=0即表示SIGNAL狀態*/
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
//tryRelease方法獲取狀態並減去releases的值,如果爲0表示鎖完全被釋放
int c = getState() - releases;
//只有持有鎖的線程才能進行釋放動作
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
//鎖被釋放,獨佔線程設置爲null
setExclusiveOwnerThread(null);
}
//更改狀態
setState(c);
return free;
}
/**
* Wakes up node's successor, if one exists.
*喚醒node節點的後繼
* @param node the node
*/
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
//如果當前節點爲SIGNAL則修改爲0
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
// 若後續節點爲空或已被cancel,則從尾部開始找到隊列中第一個waitStatus<=0,即未被cancel的節點
Node s = node.next;
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;
}
//如果找到這麼一個節點它的狀態<=0則喚醒該節點中的線程
if (s != null)
LockSupport.unpark(s.thread);
}
以上就是ReentrantLock的lock和unlock的一個簡單分析,該類中還有很多其他的方法值得我們去探究,有興趣的同學可以結合源碼自行分析。