鎖是最常用的同步方法之一,在高併發的環境下激烈的鎖競爭會導致程序的性能下降,所以我們自然有必要深入的學習一下鎖的相關知識。
在介紹Lock之前,我們需要先熟悉一個非常重要的基礎組件,JUC包下的核心基礎組件。也是實現大部分同步需求的基礎。學習該組件是學習JUC繞不開的一塊內容。該組件就是AQS。
AQS簡介
- AQS:AbstractQueuedSynchronizer,即隊列同步器。它是構建鎖或者其他同步組件的基礎框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等)。
- AQS解決了子類實現同步器時涉及當的大量細節問題,例如獲取同步狀態、FIFO同步隊列。基於AQS來構建同步器可以帶來很多好處。自定義同步器在實現時只需要實現共享資源state的獲取與釋放方式即可,至於具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實現好了,所以使用AQS不僅能夠極大地減少實現工作,而且也不必處理在多個位置上發生的競爭問題。
- 在基於AQS構建的同步器中,只能在一個時刻發生阻塞,從而降低上下文切換的開銷,提高了吞吐量。同時在設計AQS時充分考慮了可伸縮行,因此J.U.C中所有基於AQS構建的同步器均可以獲得這個優勢。
- AQS的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態。
- AQS使用一個int類型的成員變量state來表示同步狀態,當state>0時表示已經獲取了鎖,當state = 0時表示釋放了鎖。它提供了三個方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))來對同步狀態state進行操作,當然AQS可以確保對state的操作是安全的。
- AQS通過內置的FIFO同步隊列來完成資源獲取線程的排隊工作,如果當前線程獲取同步狀態失敗(鎖)時,AQS則會將當前線程以及等待狀態等信息構造成一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,則會把節點中的線程喚醒,使其再次嘗試獲取同步狀態。
AQS常用方法
關於state的方法主要有一下三種
-
getState():返回同步狀態的當前值;
-
setState(int newState):設置當前同步狀態;
-
compareAndSetState(int expect, int update):使用CAS設置當前狀態,該方法能夠保證狀態設置的原子性;
自定義同步器實現時主要實現以下幾種方法
-
tryAcquire(int arg):獨佔式獲取同步狀態,獲取同步狀態成功後,其他線程需要等待該線程釋放同步狀態才能獲取同步狀態
-
tryRelease(int arg):獨佔式釋放同步狀態;
-
tryAcquireShared(int arg):共享式獲取同步狀態,返回值大於等於0則表示獲取成功,否則獲取失敗;
-
tryReleaseShared(int arg):共享式釋放同步狀態;
-
isHeldExclusively():當前同步器是否在獨佔式模式下被線程佔用,一般該方法表示是否被當前線程所獨佔;
其餘方法
-
acquire(int arg):獨佔式獲取同步狀態,如果當前線程獲取同步狀態成功,則由該方法返回,否則,將會進入同步隊列等待,該方法將會調用可重寫的tryAcquire(int arg)方法;
-
acquireInterruptibly(int arg):與acquire(int arg)相同,但是該方法響應中斷,當前線程爲獲取到同步狀態而進入到同步隊列中,如果當前線程被中斷,則該方法會拋出InterruptedException異常並返回;
-
tryAcquireNanos(int arg,long nanos):超時獲取同步狀態,如果當前線程在nanos時間內沒有獲取到同步狀態,那麼將會返回false,已經獲取則返回true;
-
acquireShared(int arg):共享式獲取同步狀態,如果當前線程未獲取到同步狀態,將會進入同步隊列等待,與獨佔式的主要區別是在同一時刻可以有多個線程獲取到同步狀態;
-
acquireSharedInterruptibly(int arg):共享式獲取同步狀態,響應中斷;
-
tryAcquireSharedNanos(int arg, long nanosTimeout):共享式獲取同步狀態,增加超時限制;
-
release(int arg):獨佔式釋放同步狀態,該方法會在釋放同步狀態之後,將同步隊列中第一個節點包含的線程喚醒;
-
releaseShared(int arg):共享式釋放同步狀態;
CLH
CLH同步隊列是一個FIFO雙向隊列,AQS依賴它來完成同步狀態的管理,當前線程如果獲取同步狀態失敗時,AQS則會將當前線程已經等待狀態等信息構造成一個節點(Node)並將其加入到CLH同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點喚醒(公平鎖),使其再次嘗試獲取同步狀態。
在CLH同步隊列中,一個節點表示一個線程,它保存着線程的引用(thread)、狀態(waitStatus)、前驅節點(prev)、後繼節點(next),其數據結構如下
其實就是個雙端雙向鏈表。
數據定義如下
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; /** * 節點在等待隊列中,節點線程等待在Condition上,當其他線程對Condition調用了signal()後,改節點將會從等待隊列中轉移到同步隊列中,加入到同步狀態的獲取中 */ static final int CONDITION = -2; /** * 表示下一次共享式同步狀態獲取將會無條件地傳播下去 */ static final int PROPAGATE = -3; /** 等待狀態 */ volatile int waitStatus; /** 前驅節點 */ volatile Node prev; /** 後繼節點 */ volatile Node next; /** 獲取同步狀態的線程 */ volatile Thread thread; 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() { } Node(Thread thread, Node mode) { this.nextWaiter = mode; this.thread = thread; } Node(Thread thread, int waitStatus) { this.waitStatus = waitStatus; this.thread = thread; } }
可以看到AQS支持兩種同步模式,分別是Exclusive(獨佔,只有一個線程能執行,如ReentrantLock)和Share(共享,多個線程可同時執行,如Semaphore/CountDownLatch)。這樣方便使用者實現不同類型的同步組件。簡而言之,AQS爲使用者提供了多樣的底層支撐,具體如何組裝實現,使用者可以自由發揮。
入列
CLH這種鏈表式結構入列,無非就是tail指向新節點、新節點的前驅節點指向當前最後的節點,當前最後一個節點的next指向當前節點,直接看源碼相關操作在addWaiter(Node node)方法裏。此方法用於將當前線程加入到等待隊列的隊尾,並返回當前線程所在的結點
private Node addWaiter(Node mode) { //根據給定的模式(獨佔或者共享)新建Node Node node = new Node(Thread.currentThread(), mode); //快速嘗試添加尾節點 Node pred = tail; if (pred != null) { node.prev = pred; //CAS設置尾節點 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //多次嘗試 enq(node); return node; }
addWaiter(Node node)先通過快速嘗試設置尾節點,如果失敗,則調用enq(Node node)方法設置尾節點
private Node enq(final Node node) { //多次嘗試,直到成功爲止 for (;;) { Node t = tail; //tail不存在,設置爲首節點 if (t == null) { if (compareAndSetHead(new Node())) tail = head; } else { //設置爲尾節點 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
此方法用於將node加入隊尾,該方法核心就是通過CAS自旋的方式來設置尾節點,知道獲得預期的結果即添加節點成功,當前線程纔會返回。(這種方式很經典AtomicInteger.getAndIncrement()方法也是這樣做的)
出列
CLH同步隊列遵循FIFO(先進先出),首節點的線程釋放同步狀態後,將會喚醒它的後繼節點(next),而後繼節點將會在獲取同步狀態成功時將自己設置爲首節點,這個過程非常簡單,head執行該節點並斷開原首節點的next和當前節點的prev即可,注意在這個過程是不需要使用CAS來保證的,因爲只有一個線程能夠成功獲取到同步狀態。
同步狀態的獲取與釋放
AQS的設計模式採用的模板方法模式,子類通過繼承的方式,實現它的抽象方法來管理同步狀態,對於子類而言它並沒有太多的活要做,AQS提供了大量的模板方法來實現同步,主要是分爲三類:獨佔式獲取和釋放同步狀態、共享式獲取和釋放同步狀態、查詢同步隊列中的等待線程情況。自定義子類使用AQS提供的模板方法就可以實現自己的同步語義。
獨佔式同步狀態獲取
此方法是獨佔模式下線程獲取共享資源的頂層入口。如果獲取到資源,線程直接返回,否則進入等待隊列,直到獲取到資源爲止,且整個過程忽略中斷的影響。這也正是lock()的語義,當然不僅僅只限於lock()。也就是說由於線程獲取同步狀態失敗加入到CLH同步隊列中,後續對線程進行中斷操作時,線程不會從同步隊列中移除獲取到資源後。下面是acquire()的源碼:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
-
tryAcquire:去嘗試獲取鎖,獲取成功則設置鎖狀態並返回true,否則返回false。該方法由自定義同步組件自己實現(通過state的get/set/CAS),該方法必須要保證線程安全的獲取同步狀態。
-
addWaiter:如果tryAcquire返回FALSE(獲取同步狀態失敗),則調用該方法將當前線程加入到CLH同步隊列尾部,並標記爲獨佔模式。
-
acquireQueued:當前線程會根據公平性原則來進行阻塞等待(自旋),直到獲取鎖爲止;如果在整個等待過程中被中斷過,則返回true,否則返回false。
-
selfInterrupt:如果線程在等待過程中被中斷過,它是不響應的。只是獲取資源後纔再進行自我中斷selfInterrupt(),將中斷補上。
tryAcquire(int)
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
該方法直接拋出異常,具體實現交自定義同步器類實現。這裏之所以沒有定義成abstract,是因爲獨佔模式下只用實現tryAcquire-tryRelease,而共享模式下只用實現tryAcquireShared-tryReleaseShared。如果都定義成abstract,那麼每個模式也要去實現另一模式下的接口。
acquireQueued
在執行到此方法時已經說明一點:該線程獲取資源失敗,已經被放入等待隊列尾部了。所以 acquireQueued方法就是讓線程進入等待狀態休息,直到其他線程徹底釋放資源後喚醒該線程,獲取所需資源,然後執行該線程所需執行的任務。
acquireQueued方法爲一個自旋的過程,也就是說當前線程(Node)進入同步隊列後,就會進入一個自旋的過程,每個節點都會自我觀察,當條件滿足,獲取到同步狀態後,就可以從這個自旋過程中退出,否則會一直執行下去。
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)) { /*將head指向該節點*/ setHead(node); /* 方便GC回收垃圾 */ p.next = null; failed = false; /*返回等待過程中是否被中斷過*/ return interrupted; } /*獲取失敗,線程就進入waiting狀態,直到被unpark()*/ if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()) /*如果等待過程中被中斷過一次,就標記爲true*/ interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
從上面代碼中可以看到,當前線程會一直嘗試獲取同步狀態,當然前提是隻有其前驅節點爲頭結點才能夠嘗試獲取同步狀態,理由:
-
保持FIFO同步隊列原則。
-
頭節點釋放同步狀態後,將會喚醒其後繼節點,後繼節點被喚醒後需要檢查自己是否爲頭節點。
shouldParkAfterFailedAcquire(Node, Node)
此方法主要用於檢查狀態,查看當前節點是否進入waiting狀態
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus;//拿到前驅節點的狀態 if (ws == Node.SIGNAL) //狀態爲SIGNAL,如果前驅節點處於等待狀態,直接返回true return true; if (ws > 0) { /* * 如果前驅節點放棄了,那就一直往前找,直到找到最近一個正常等待的狀態,並排在它的後邊。 * 注意:那些放棄的結點,由於被自己“加塞”到它們前邊,它們相當於形成一個無引用鏈,稍後就會被GC回收 */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { //如果前驅節點正常,那就把前驅的狀態通過CAS的方式設置成SIGNAL compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
這段代碼主要檢查當前線程是否需要被阻塞,具體規則如下:
-
如果當前線程的前驅節點狀態爲SINNAL,則表明當前線程需要被阻塞,調用unpark()方法喚醒,直接返回true,當前線程阻塞
-
如果當前線程的前驅節點狀態爲CANCELLED(ws > 0),則表明該線程的前驅節點已經等待超時或者被中斷了,則需要從CLH隊列中將該前驅節點刪除掉,直到回溯到前驅節點狀態 <= 0 ,返回false
-
如果前驅節點非SINNAL,非CANCELLED,則通過CAS的方式將其前驅節點設置爲SINNAL,返回false
整個流程中,如果前驅結點的狀態不是SIGNAL,那麼自己就不能被阻塞,需要去找個安心的休息點(前驅節點狀態 <= 0 ),同時可以再嘗試下看有沒有機會去獲取資源。
如果 shouldParkAfterFailedAcquire(Node pred, Node node) 方法返回true,則調用parkAndCheckInterrupt()方法阻塞當前線程:
private final boolean parkAndCheckInterrupt() { //調用park()使線程進入waiting狀態 LockSupport.park(this); //如果被喚醒,查看自己是不是被中斷的 return Thread.interrupted(); }
parkAndCheckInterrupt() 方法主要是把當前線程掛起,從而阻塞住線程的調用棧,同時返回當前線程的中斷狀態。
深入理解:
談到併發,不得不談ReentrantLock
;而談到ReentrantLock
,不得不談AbstractQueuedSynchronized(AQS)!,類如其名,抽象的隊列式的同步器,AQS定義了一套多線程訪問共享資源的同步器框架,許多同步類實現都依賴於它,如常用的ReentrantLock/Semaphore/CountDownLatch...。我們以ReentrantLock作爲講解切入點。
1. ReentrantLock的調用過程
ReentrantLock把所有Lock接口的操作都委派到一個Sync類上,該類繼承了AbstractQueuedSynchronizer:
static abstract class Sync extends AbstractQueuedSynchronizer
Sync又有兩個子類:
final static class NonfairSync extends Sync
final static class FairSync extends Sync
顯然是爲了支持公平鎖和非公平鎖而定義,默認情況下爲非公平鎖。
先理一下Reentrant.lock()方法的調用過程(默認非公平鎖):
0_13119022769n5R.gif
2. 鎖實現(加鎖)
簡單說來,AbstractQueuedSynchronizer會把所有的請求線程構成一個CLH隊列,當一個線程執行完畢(lock.unlock())時會激活自己的後繼節點,但正在執行的線程並不在隊列中,而那些等待執行的線程全部處於阻塞狀態.
線程的顯式阻塞是通過調用LockSupport.park()完成,而LockSupport.park()則調用sun.misc.Unsafe.park()本地方法,再進一步,HotSpot在Linux中中通過調用pthread_mutex_lock函數把線程交給系統內核進行阻塞。該隊列如圖:
0_1311847049xnXb.gif
與synchronized相同的是,這也是一個虛擬隊列,不存在隊列實例,僅存在節點之間的前後關係。令人疑惑的是爲什麼採用CLH隊列呢?原生的CLH隊列是用於自旋鎖,但Doug Lea把其改造爲阻塞鎖。
當有線程競爭鎖時,該線程會首先嚐試獲得鎖,這對於那些已經在隊列中排隊的線程來說顯得不公平,這是非公平鎖的由來之一,與synchronized實現類似,這樣會極大提高吞吐量。
如果已經存在Running線程,則新的競爭線程會被追加到隊尾,具體是採用基於CAS的Lock-Free算法,因爲線程併發對Tail調用CAS可能會導致其他線程CAS失敗,解決辦法是循環CAS直至成功。
AbstractQueuedSynchronizer的實現非常精巧,令人歎爲觀止,不入細節難以完全領會其精髓,下面詳細說明實現過程:
2.1 Sync.nonfairTryAcquire
nonfairTryAcquire方法將是lock方法間接調用的第一個方法,每次請求鎖時都會首先調用該方法。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
1.該方法會首先判斷當前狀態,如果c==0說明沒有線程正在競爭該鎖,如果不c !=0 說明有線程正擁有了該鎖。
2.如果發現c==0,則通過CAS設置該狀態值爲acquires,acquires的初始調用值爲1,每次線程重入該鎖都會+1,每次unlock都會-1,但爲0時釋放鎖。如果CAS設置成功,則可以預計其他任何線程調用CAS都不會再成功,也就認爲當前線程得到了該鎖,也作爲Running線程,很顯然這個Running線程並未進入等待隊列。
3.如果c !=0 但發現自己已經擁有鎖,只是簡單地++acquires,並修改status值,但因爲沒有競爭,所以通過setStatus修改,而非CAS,也就是說這段代碼實現了偏向鎖的功能,並且實現的非常漂亮。
2.2 AbstractQueuedSynchronizer.addWaiter
addWaiter方法負責把當前無法獲得鎖的線程包裝爲一個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;
}
其中參數mode是獨佔鎖還是共享鎖,默認爲null,獨佔鎖。追加到隊尾的動作分兩步:
1.如果當前隊尾已經存在(tail!=null),則使用CAS把當前線程更新爲Tail
2.如果當前Tail爲null或則線程調用CAS設置隊尾失敗,則通過enq方法繼續設置Tail
下面是enq方法:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
Node h = new Node(); // Dummy header
h.next = node;
node.prev = h;
if (compareAndSetHead(h)) {
tail = node;
return h;
}
}
else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
該方法就是循環調用CAS,即使有高併發的場景,無限循環將會最終成功把當前線程追加到隊尾(或設置隊頭)。總而言之,addWaiter的目的就是通過CAS把當前現在追加到隊尾,並返回包裝後的Node實例。
把線程要包裝爲Node對象的主要原因,除了用Node構造供虛擬隊列外,還用Node包裝了各種線程狀態,這些狀態被精心設計爲一些數字值:
- SIGNAL(-1) :線程的後繼線程正/已被阻塞,當該線程release或cancel時要重新這個後繼線程(unpark)
- CANCELLED(1):因爲超時或中斷,該線程已經被取消
- CONDITION(-2):表明該線程被處於條件隊列,就是因爲調用了>- Condition.await而被阻塞
- PROPAGATE(-3):傳播共享鎖
- 0:0代表無狀態
2.3 AbstractQueuedSynchronizer.acquireQueued
acquireQueued的主要作用是把已經追加到隊列的線程節點(addWaiter方法返回值)進行阻塞,但阻塞前又通過tryAccquire重試是否能獲得鎖,如果重試成功能則無需阻塞,這裏是非公平鎖的由來之二
final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} catch (RuntimeException ex) {
cancelAcquire(node);
throw ex;
}
}
仔細看看這個方法是個無限循環,感覺如果p == head && tryAcquire(arg)條件不滿足循環將永遠無法結束,當然不會出現死循環,奧祕在於第12行的parkAndCheckInterrupt會把當前線程掛起,從而阻塞住線程的調用棧。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
如前面所述,LockSupport.park最終把線程交給系統(Linux)內核進行阻塞。當然也不是馬上把請求不到鎖的線程進行阻塞,還要檢查該線程的狀態,比如如果該線程處於Cancel狀態則沒有必要,具體的檢查在shouldParkAfterFailedAcquire中:
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;
}
檢查原則在於:
規則1:如果前繼的節點狀態爲SIGNAL,表明當前節點需要unpark,則返回成功,此時acquireQueued方法的第12行(parkAndCheckInterrupt)將導致線程阻塞
規則2:如果前繼節點狀態爲CANCELLED(ws>0),說明前置節點已經被放棄,則回溯到一個非取消的前繼節點,返回false,acquireQueued方法的無限循環將遞歸調用該方法,直至規則1返回true,導致線程阻塞
規則3:如果前繼節點狀態爲非SIGNAL、非CANCELLED,則設置前繼的狀態爲SIGNAL,返回false後進入acquireQueued的無限循環,與規則2同
總體看來,shouldParkAfterFailedAcquire就是靠前繼節點判斷當前線程是否應該被阻塞,如果前繼節點處於CANCELLED狀態,則順便刪除這些節點重新構造隊列。
至此,鎖住線程的邏輯已經完成,下面討論解鎖的過程。
3. 解鎖
請求鎖不成功的線程會被掛起在acquireQueued方法的第12行,12行以後的代碼必須等線程被解鎖鎖才能執行,假如被阻塞的線程得到解鎖,則執行第13行,即設置interrupted = true,之後又進入無限循環。
從無限循環的代碼可以看出,並不是得到解鎖的線程一定能獲得鎖,必須在第6行中調用tryAccquire重新競爭,因爲鎖是非公平的,有可能被新加入的線程獲得,從而導致剛被喚醒的線程再次被阻塞,這個細節充分體現了“非公平”的精髓。通過之後將要介紹的解鎖機制會看到,第一個被解鎖的線程就是Head,因此p == head的判斷基本都會成功。
至此可以看到,把tryAcquire方法延遲到子類中實現的做法非常精妙並具有極強的可擴展性,令人歎爲觀止!當然精妙的不是這個Templae設計模式,而是Doug Lea對鎖結構的精心佈局。
解鎖代碼相對簡單,主要體現在AbstractQueuedSynchronizer.release和Sync.tryRelease方法中:
class AbstractQueuedSynchronizer
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
class Sync
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;
}
tryRelease與tryAcquire語義相同,把如何釋放的邏輯延遲到子類中。tryRelease語義很明確:如果線程多次鎖定,則進行多次釋放,直至status==0則真正釋放鎖,所謂釋放鎖即設置status爲0,因爲無競爭所以沒有使用CAS。
release的語義在於:如果可以釋放鎖,則喚醒隊列第一個線程(Head),具體喚醒代碼如下:
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);
}
這段代碼的意思在於找出第一個可以unpark的線程,一般說來head.next == head,Head就是第一個線程,但Head.next可能被取消或被置爲null,因此比較穩妥的辦法是從後往前找第一個可用線程。貌似回溯會導致性能降低,其實這個發生的機率很小,所以不會有性能影響。之後便是通知系統內核繼續該線程,在Linux下是通過pthread_mutex_unlock完成。之後,被解鎖的線程進入上面所說的重新競爭狀態。
4. Lock VS Synchronized
AbstractQueuedSynchronizer通過構造一個基於阻塞的CLH隊列容納所有的阻塞線程,而對該隊列的操作均通過Lock-Free(CAS)操作,但對已經獲得鎖的線程而言,ReentrantLock實現了偏向鎖的功能。
synchronized的底層也是一個基於CAS操作的等待隊列,但JVM實現的更精細,把等待隊列分爲ContentionList和EntryList,目的是爲了降低線程的出列速度;當然也實現了偏向鎖,從數據結構來說二者設計沒有本質區別。但synchronized還實現了自旋鎖,並針對不同的系統和硬件體系進行了優化,而Lock則完全依靠系統阻塞掛起等待線程。
當然Lock比synchronized更適合在應用層擴展,可以繼承AbstractQueuedSynchronizer定義各種實現,比如實現讀寫鎖(ReadWriteLock),公平或不公平鎖;同時,Lock對應的Condition也比wait/notify要方便的多、靈活的多。