前言
之前看過美團的一篇不可不說的Java“鎖”事,對java鎖的概念做了一次梳理,其實在java類中,ReentrantLock算是一個對鎖概念運用的典範,看懂它的源碼對鎖的理解很有幫助。我也以ReentrantLock爲原型,略加改動使之能在分佈式環境中運行。
幕後功臣AQS
當我們看第一眼ReentrantLock源碼,裏面有一個Sync對象,它繼承AbstractQueuedSynchronizer。
AbstractQueuedSynchronizer是一個抽象的隊列式的同步框架,AQS定義了一套多線程訪問共享資源的同步器框架,許多同步類實現都依賴於它,如常用的ReentrantLock,CountDownLatch
下面代碼複寫了tryRelease方法,其餘的一些方法是自定義方法,我先刪除了。其實還有一個tryAcquire方法,當開發複寫這兩個方法,就可以完成一個鎖的編碼設計。
- tryAcquire 嘗試獲取鎖
- tryRelease 嘗試釋放鎖
是不是感覺有了這個萬能的框架後,寫一個鎖很簡單了?AbstractQueuedSynchronizer遵循 模板設計 模式,主骨架已經給你搭好,爲了能串聯起整個功能,你必須要複寫必要的方法,不然直接調用AQS的方法,會拋出UnsupportedOperationException異常。
所以對ReentrantLock源碼的研究也是對AQS的研究。
private final Sync sync;
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}
ReentrantLock的大致流程
ReentrantLock的獲取鎖流程
上鎖的流程可以直接看AQS的acquire方法
- tryAcquire: 不用多說了,先去嘗試獲取鎖
- addWaiter: 如果獲取鎖不成功,便將當前線程包裝成Node對象,加入到FIFO隊列。
- acquireQueued: 這裏是比較重要的邏輯,線程是否休眠的判斷邏輯,線程休眠(wait)的邏輯,線程在隊列裏位置調整的邏輯。這個方法最重要的還是讓線程休眠,等待喚醒。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
然後根據 && 的短路特性,我們把上面的步驟總結一下:
先嚐試獲取鎖(tryAcquire),發現獲取失敗,則加入等待隊列(addWaiter),並且判斷是否需要休眠,是的話則休眠,不是則重試獲取鎖(acquireQueued)。
ReentrantLock的釋放鎖流程
- tryRelease 釋放鎖的方法
- unparkSuccessor 如果存在的話,喚醒後繼節點
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
釋放鎖的步驟簡單:先嚐試釋放鎖,成功的話喚醒head節點的後繼節點.
詳解ReentrantLock
我想根據鎖的特性(可重入,公平鎖,自旋)來 拆解 講解ReentrantLock,可以加深對鎖的理解。
樂觀 or 悲觀 ?
樂觀鎖與悲觀鎖是一種廣義上的概念,在ReentrantLock的實現中,我們要從整體和部分兩部分來說。
從整體來看,它是一個悲觀鎖,因爲它是一個獨佔鎖,當一個線程持有它並釋放它前,其他線程是不能讀或寫共享資源的。
但從部分來看,ReentrantLock的實現方式有用到樂觀鎖(CAS)。
在AQS中,它維護了一個state變量(代表共享資源),ReentrantLock利用state變量來維護鎖的獲取狀態,看代碼
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//初始狀態下,state爲0,代表鎖還沒有線程獲取。
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
//當鎖獲取成功時,state變成1(acquires爲1)
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
。。。。。
上圖的部分代碼可以知道,state初始化爲0,表示未鎖定狀態。線程
調用時,會調用tryAcquire()獨佔該鎖並將state設爲acquires值(默認爲1)。此後,其他線程再tryAcquire()時就會失敗,直到線程unlock()到state=0(即釋放鎖)爲止,其它線程纔有機會獲取該鎖。
如何保證只有當state爲0時更新才能成功?這就是compareAndSetState方法去做的事,compareAndSetState使用一種比較交換的技術(CAS Compare And Swap)來鑑別線程衝突,一旦檢測到衝突的產生,更新會失敗,並且線程不會阻塞掛起(跟悲觀鎖的主要區別),而是返回操作結果,你後面需要重試或者報錯都取決於你,它是一個原子操作,所以在多線程中,CAS可以保證同步,這也就是樂觀鎖的一個經典實現。
CAS的更多內容網上有一堆,大家可以拓展閱讀。所以在對一個鎖的實現進行定性時,不能陷入一種誤區,就是這個鎖一定是悲觀 or 樂觀,需要根據鎖的內部實現方式,服務對象來綜合判斷。
可重入性
重入鎖 意思是 同一個線程 能夠 多次獲取同一個鎖,並且不產生死鎖。可能會問什麼情況會這個線程多次獲取同一個鎖?下面是最簡單的示例,test方法調用test1方法,兩個方法都用synchronized修飾,如果synchronized沒有可重入的特性,線程執行到test1的時候會因爲沒有鎖而卡住(test的鎖還沒有釋放)。
synchronized void test() {
test1();
}
synchronized void test1() {
}
ReentrantLock名字本身就表示它是一個可重入鎖,繼續從代碼看看怎麼實現的吧。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
這個代碼還是取的上面那個代碼,只是這次我們得關注重心在 else if代碼塊中。
當getState()返回>0時,說明這個線程已經持有該鎖了,但是還沒有釋放鎖,然後他它會先判斷當前線程是不是持有該鎖的線程(兩個線程不相等當然不能執行操作),這是可重入的前提,必須是同一個線程,
current == getExclusiveOwnerThread()
判斷完沒毛病後,就是簡單的對state變量進行加法操作,你重入了2次,state就爲2,重入3次就是3。可以看到這部分拿鎖操作連CAS都沒用,效率會非常快。
在解鎖方面,大家也應該想到了,就是對state-1,直到state爲0
//releases 默認爲1
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
//當然也會判斷一下當前鎖的持有線程是不是跟 當前執行的線程一致
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果state減爲0,則釋放鎖,否則只是更新一下state變量,重入了幾次,就要解鎖幾次。
if (c == 0) {
free = true;
//鎖持有線程清空
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
可重入鎖解決了實際使用可能的死鎖問題,而且性能消耗比線程第一次獲取鎖小很多.到目前爲止,我們已經看完了ReentrantLock的門面設計,說前面的都是門面,因爲ReentrantLock還有一個重要的組成部分還沒介紹,就是先進先出的隊列,用於暫存被睡眠的線程,我們由公平鎖來引出它。
公平 or 非公平
讓我們回到獲取鎖的流程,tryAcquire方法如果失敗了,就會被丟進一個等待隊列,先從addWaiter開始。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
addWaiter 和 enq方法都被我摘出來。當一個節點獲取鎖失敗時,會被放入隊列中,隊列本身的插入邏輯與一般維護隊列的代碼沒什麼區別,但怎麼保證線程安全性?還是利用了CAS來保證。
在enq中,如果這個隊列本身不存在,會進行一個循環(enq),先建一個頭節點,這個頭節點沒有綁定線程信息,然後需要加入隊列的節點Node會放在尾節點。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
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 (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
addWaiter結束以後,就輪到acquireQueued。這裏的邏輯有點繞,剛進去就是一個死循環
當前線程node稱爲 節點A
- 先判斷 節點A 的 前置節點 是不是頭節點
1.1 是的話說明可以直接 嘗試獲取 鎖(tryAcquire),獲取成功,則 節點A 設爲Head節點,前置節點脫離鏈表(p.next = null)
1.2 不是的話進入 <2> 步驟 - shouldParkAfterFailedAcquire
2.1 先判斷 前置節點 是不是已經是 **請求釋放(Node.SIGNAL) ** 的狀態,是的話 返回true,接着直接執行 parkAndCheckInterrupt 方法掛起 節點A 所持有的當前線程。
2.2 如果 前置節點 不是 **請求釋放(Node.SIGNAL) ** 的狀態,且waitStatus狀態值>0,說明 前置節點 已經被取消,就需要找前置節點的前置,一直找到waitStatus狀態 <=0爲止(do while 循環)作爲 節點A 的新前置節點。
2.3 如果 前置節點 不是 **請求釋放(Node.SIGNAL) ** 的狀態,且waitStatus狀態值<=0 ,說明 前置節點 的waitStatus狀態需要被設置成 請求釋放(Node.SIGNAL) 的狀態,但是現在不能讓它掛起,而是繼續<1>的步驟。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
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 {
/*
* 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.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
經過上面的循環,節點A 有兩個歸宿,一個是拿鎖成功返回,一個是被掛起,而掛起後怎麼被 喚醒再繼續 上面的步驟<1>的,玄機在解鎖的代碼。
假設tryRelease返回爲true
- 取 頭節點 Head,頭節點非空且狀態不是初始態(0),進入喚醒流程
- 頭節點 狀態如果 < 0,重置頭節點爲0(CAS)
- 取頭節點的 後繼節點 ,判斷 後繼節點 的waitStatus是不是取消態(waitStatus>0),是的話重新找離頭節點最近的後繼節點,從尾節點往前找
- 最後喚醒這個 後繼節點 。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
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)
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.
*/
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;
}
if (s != null)
LockSupport.unpark(s.thread);
}
在喚醒和睡眠的代碼裏,邏輯比較繞的,就是對waitStatus狀態的變更,而總結一句話就是:
喚醒前,總會把 Head頭節點 設爲 0(初始態),並且喚醒節點成功獲取鎖之後會取代頭節點位置(acquireQueued setHead(node))
睡眠前,總會把前置節點設爲 -1 (請求釋放(Node.SIGNAL) )
介紹完AQS對隊列的使用,我們可以看到對公平鎖和非公平鎖的使用
公平鎖: 按照字面意思理解很簡單,下一個處理的線程一定是按照隊列排序的,即使是新來的線程,也必須先進隊列再操作。
FairSync是ReentrantLock的公平鎖實現類,裏面的tryAcquire方法跟NonfairSync(非公平鎖)唯一不同的就是多了一個hasQueuedPredecessors判斷
hasQueuedPredecessors的意思就是每個新來的線程必要要先判斷 隊列裏是不是已經有等待的線程,如果自己不是隊列裏第一個線程,就不允許嘗試獲取鎖。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
。。。。。
}
非公平鎖: 首先要更正一個誤區,即使是非公平鎖,也大致保持着公平的特性,如果沒有新來的線程,對鎖的獲取也是先來後到的原則(隊列來保證),只是如果有新來的線程,就可以插隊,而插隊不插隊的判斷邏輯就是有沒有使用hasQueuedPredecessors方法(NonfairSync實現類是沒有使用hasQueuedPredecessors方法)