一、前言
上一篇《Java鎖之ReentrantLock(一)》已經介紹了ReentrantLock的基本原始碼,分析了ReentrantLock的公平鎖和非公平鎖機制,最終分析ReentrantLock還是依託於
AbstractQueuedSynchronizer
同步佇列器(以下簡稱同步器)實現,所以本篇開始分析同步器內部的程式碼實現,考慮到程式碼結構比較長,所以分析原始碼時會精簡部分不重要的程式碼,但是最終還是會以不影響程式碼邏輯的情況下進行精簡。
二、同步器分析
-
同步器主要屬性
根據上圖原始碼我們可以知道,
AbstractQueuedSynchronizer
內部構建了一個Node節點物件,同時構造了一個具有volatile屬性頭節點與尾部節點,保證了多執行緒之間的可見性,同時最重要的是定義了一個int型別變數state,通過上一篇文章分析,我們知道了ReenTrantLock是否獲取到鎖的判斷就是state是否大於0,等於0表示鎖空閒,大於0,表示鎖已經被獲取。接下來我們重點分析下Node節點內部構造以及同步器的實現原理,Node原始碼如下:
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 int waitStatus;
//上一個節點
volatile Node prev;
//下一個節點
volatile Node next;
//獲取同步狀態的執行緒
volatile Thread thread;
//等待佇列中的後繼節點,如果當前節點是共享的,那麼這個nextWaiter=SHARED
Node nextWaiter;
//判斷當前後繼節點是否是共享的
final boolean isShared() {
return nextWaiter == SHARED;
}
//返回當前節點的前一個節點
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
複製程式碼
這裡需要重點說明的屬性是
waitStatus
,該狀態就是包括節點內部宣告的幾個常量,如下:
常量名 | 功能 |
---|---|
CANCELLED | 值為1,當前節點進入取消狀態,原因是由於被中斷或者是等待超時而進入取消狀態,需要說明的是,節點執行緒進入取消狀態後,狀態不會再改變,也就不會再阻塞獲取鎖 |
SIGNAL | 值為-1,後繼節點的執行緒處於等待狀態,而當前節點的執行緒如果釋放了同步狀態或者被取消,將會通知後繼節點,使得後繼節點的執行緒得以執行 |
CONDITION | 值為-2,節點在等待佇列中,節點執行緒等待在Condition上,當其他執行緒對Condition呼叫了signal()方法後,該節點將會從等待佇列中轉移到同步佇列中,加入到同步狀態的競爭中 |
PROPAGATE | 值為-3,表示下一次共享式同步狀態獲取會無條件的傳播下去,比如如果頭節點獲取到共享式同步狀態,判斷狀態是PROPAGATE,會繼續呼叫doReleaseShared,使得後繼節點繼續獲取鎖 |
INITIAL | 值為 0,表示初始狀態(這個應該是老版本中的程式碼中存在,目前檢視jdk1.8已經沒有顯示宣告INITIAL狀態,因為初始化時候,int變數預設就是0) |
分析同步器的屬性,我們可以大概畫出構造器的佇列示意圖,如下:
首先同步器宣告瞭頭節點和尾部節點,head節點指向一個node節點表示該節點是佇列的頭部節點,tail節點指向一個node節點表示該節點是尾部節點,同時,每個節點都有pre和next屬性,指向node節點,然後如圖所示構建成一個FIFO雙向連結串列式佇列。下面我們檢視下同步器常用主要方法
-
同步器主要方法列表
方法名稱 | 功能 |
---|---|
compareAndSetState(int expect, int update) | CAS進行設定同步狀態 |
enq(final Node node) | 迴圈入等待佇列,直到入隊成功為止 |
addWaiter(Node mode) | 以當前執行緒建立一個尾部節點,並加入到尾部 |
unparkSuccessor(Node node) | 喚醒節點的後繼節點 |
doReleaseShared() | 釋放共享模式下的同步狀態 |
setHeadAndPropagate(Node node, int propagate) | 設定頭節點,並繼續傳播同步許可 |
release(int arg) | 獨佔式釋放同步狀態 |
acquireShared(int arg) | 共享式釋放同步狀態 |
hasQueuedPredecessors() | 判斷是否有比當前執行緒等待更久的執行緒(用於公平鎖) |
-
同步器加鎖
以上是同步器主要的方法,我們接下來會對上述部分方法進行重點分析,要了解同步器如何完成加鎖,等待獲取鎖,釋放鎖的功能,我們先回顧上一篇文章分析ReentranLock的Lock()方法,實現原始碼如下:
/**
* 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);
}
複製程式碼
我們發現重點是acquire(1)方法,該方法是父類也就是同步器提供的,原始碼如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
複製程式碼
原始碼其實可以拆分為三部分:
tryAcquire(arg)
嘗試獲取鎖addWaiter(Node.EXCLUSIVE), arg)
以當前執行緒構建成節點新增到佇列尾部acquireQueued(final Node node, int arg)
讓節點以死迴圈去獲取同步狀態,獲取成功就退出迴圈
其實解析為三部分就很清楚這個方法的作用了,首先嚐試獲取鎖,獲取不到就把自己新增到尾部,然後在佇列中死迴圈去獲取鎖,最重要的部分就是
acquireQueued(final Node node, int arg)
,原始碼如下:
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)
/*
* 如果前任節點的狀態等於SIGNAL,
* 說明前任節點獲取到了同步狀態,當前節點應該被阻塞,返回true
*/
return true;
if (ws > 0) {
/*
* 前任節點被取消
*/
do {//迴圈查詢取消節點的前任節點,
//直到找到不是取消狀態的節點,然後剔除是取消狀態的節點,
//關聯前任節點的下一個節點為當前節點
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* CAS設定前任節點等待狀態為SIGNAL,
* 設定成功表示當前節點應該被阻塞,下一次迴圈呼叫就會
* return true
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
複製程式碼
//把當前執行緒掛起,從而阻塞住執行緒的呼叫棧,
//同時返回當前執行緒的中斷狀態。
//其內部則是呼叫LockSupport工具類的park()方法來阻塞該方法。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//阻塞執行緒
return Thread.interrupted();
}
複製程式碼
以上就是lock.lock()的加鎖過程,我們總結分析下:
- 首先,直接嘗試獲取鎖,獲取成功直接結束。
- 如果獲取鎖失敗,就把當前執行緒構造一個尾部節點,CAS方式加入到佇列的尾部
- 在佇列中,死迴圈式的判斷前任節點是否是頭節點,如果是頭節點就嘗試獲取鎖,如果不是就把自己掛起,等待前任節點喚醒自己,這樣可以避免多個執行緒死迴圈帶來的效能消耗。
-
同步器解鎖
lock.unlock()釋放鎖的過程,分析原始碼,老規矩,上原始碼:
//釋放鎖
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
//嘗試釋放鎖
if (tryRelease(arg)) {
Node h = head;
//如果頭節點不為空,並且不是初始狀態,也就是不在佇列中了
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//喚醒後繼節點
return true;
}
return false;
}
//該方法和之前分析的程式碼類似,主要是設定Sate狀態
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);//設定同步狀態佔有執行緒為null
}
setState(c);
return free;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 取到當前節點的下一個節點,如果節點不為空就喚醒阻塞的執行緒
//如果節點為空,或者節點是取消狀態,那麼就迴圈從尾部節點找到
//當前節點的下一個節點喚醒
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);//喚醒阻塞的執行緒
}
複製程式碼
以上就是對lock.unlock()分析,同樣我們總結分析下
- 首先既然加鎖成功與否判斷是根據State不為0來判斷,所以,釋放鎖就會把State設定為0,同時設定鎖的所有者執行緒為null
- 鎖釋放成功了,接著就會喚醒在佇列的後繼節點,通過呼叫
LockSupport.unpark(s.thread)
來喚醒執行緒的,LockSupport
主要依託於sun.misc.Unsafe
類來實現的,該類提供了作業系統硬體級別的方法,不在本文討論中。
三、尾言
- 本次主要分析了AQS同步器的加鎖和解鎖的實現,其實jdk很多同步工具類都是依賴於AQS同步器實現的,瞭解了AQS同步器的原理後,對理解其他併發工具的原理也很有幫助,比如
CountDownLatch
,Semaphore
,CyclicBarrier(依賴ReentrantLock)
等。下一篇《Java鎖之ReentrantReadWriteLock》繼續細化分析,分析讀鎖和寫鎖,分析ReentrantLock是如何實現讀鎖的重複獲取,鎖降級等功能的。