Concurrent包下有許多工具類,包括共享語義和獨占語義的,他們都是以AbstractQueuedSynchronizer(以下簡稱AQS)為基礎構建的,在類內部都包含了一個繼承AQS的類。
AbstractQueuedSynchronizer
我們先談一下AQS這個類,其中我認為最重要的一個變量如下
-
synchronization state
private volatile int state;
為什么會有共享和獨占類的區分?所謂共享,就是同時可以有n個線程一起訪問,當n==1時,“共享”也就變成了“獨占”,那么這樣來說,共享和獨占之間的界限并不是不可逾越的。
上面我所說的也就是我對state這個變量含義的理解。為什么ReentrantLock和Semaphore可以由同一個基類去實現(這兩個類可以互相實現),甚至用的是同一個狀態變量?原因就在于此。
如何看待、處理這個state值的變化,造就了五花八門的工具類。
顯然,AQS內部還需要維護了一個隊列,畢竟你管并發,那么多線程,總要排個隊把。
static final class Node {
volatile int waitStatus; //表示該節點的等待狀態,如下
static final int CANCELLED = 1; //該節點對應的線程已經取消
static final int SIGNAL = -1; //要喚醒的后繼結點
static final int CONDITION = -2; //該節點的線程正在condition隊列中
static final int PROPAGATE = -3; //共享模式下會一直向后傳播“喚醒”的動作
volatile Node prev;
volatile Node next;
volatile Thread thread;
}
AQS所用的隊列是CLH隊列,這個隊列也叫做syn隊列,只有申請資源發現資源不夠的線程會加入這個syn隊列(比如Lock.lock()獲取鎖失敗),不用太關注這個隊列的實現的細節,這是一個雙向鏈表實現的FIFO隊列,某一節點的狀態(自旋,掛起,喚醒)都由自己的前驅結點的狀態決定,這也是waitStatus的意義所在。
入隊出隊時掛起線程、喚醒線程都借助的是LockSupport這個輔助類,這個類最關鍵的兩個方法就是park和unpark方法,也就是掛起和喚醒線程,這個類在這就不多展開了。
然而在AQS中,還有一個內部類,也引用了這個Node類,也就是說還有一個隊列
AQS內部為了兩個隊列,一個就是我們上面提到過的syn隊列,一個就是這個condition隊列了,任何線程要么獲取了資源,要么就在syn隊列,要么就在condition隊列,用一個很簡答的例子展示下
public class DEMO {
private static ReentrantLock lock =new ReentrantLock();
private static Condition condition=lock.newCondition();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
lock.lock(); //1.線程1獲取鎖成功
try {
condition.await();//3.線程1釋放鎖,并且加入condition隊列,此時syn隊列頭節點即線程2被喚醒
//6.線程1被喚醒繼續執行
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
});
t1.start();
Thread.sleep(1000);
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
try{
lock.lock(); //2.線程2申請獲取鎖資源,失敗,加入syn隊列
condition.signal();//4.線程2喚醒condition隊列中的頭節點即線程1,線程1進入syn隊列
}finally {
lock.unlock(); //5.線程2釋放鎖資源,喚醒syn隊列頭節點
}
}
});
t2.start();
}
其實這兩個隊列都很好理解,syn隊列裝的是要去爭取某個資源的線程,condition隊列裝的都是等待condition.signal的線程。
關于線程在兩個隊列之間的切換及condition方法的一些細節就不在這里多展開了,有興趣的讀者可以自己在ide里面閱讀ConditionObject這個類的方法,非常好懂,直接可以從名字的語義讀出這個類方法的思路。在這里我就想談一下在隊列內部關于自旋還是掛起的一些細節。
獲取資源的入口都是acquire方法
tryAcquire方法由子類實現,由邏輯短路特性得知,只有在tryAcuqire失敗后,后面的acquQueued才會執行,由名字也能看出來,這個方法是判斷是否要入隊的。
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);
}
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
單獨把這一行邏輯拿出來,這一行代碼就是判斷掛起時機的關鍵,同樣因為邏輯表達式的短路特性,只有第一個判斷該節點確實要掛起,才會執行第二個park操作,我們看下第一個表達式
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true; //因為前驅結點是SIGNAL,所以后續節點可以放心掛起
if (ws > 0) {//ws>0代表前驅結點已被取消,不斷向前移動跳過這類節點
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {//代表前驅結點狀態為0或者為PROPAGATE態
/*
* 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.
*/
//我們設置前驅結點的狀態為SIGNAL,下一次訪問的時候再掛起。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
梳理下邏輯,就是如果當前節點的前驅結點是SIGNAL狀態,那么我可以安心的掛起,但是如果是其他狀態那么就要下一次再嘗試了。而嘗試的過程,按我的理解,類似于自旋吧,就是不急于把自己掛起,特別是在上述方法的else分支,體現的淋漓盡致。
parkAndCheckInterrupt()方法比較簡單,就不多說了
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
關于資源的釋放也可以對比著分析,和獲取的過程是十分相似的,所有資源的釋放調用鏈都是從release()開始,再到tryRelease(),釋放時也是對節點的waitStatus進行判斷,最后也還是借助LockSupport,為了避免文章冗余,在這就不多重復。
這就是關于AQS這個基礎類本身我想細講的全部了,下面就分析下以這個類為基礎,Concurrent包下的一些工具類。
基于AbstractQueuedSynchronizer的共享獨占工具類
所有的工具類,都是在內部實現一個syn類去繼承AQS,并實現一些方法,此外所有資源的請求(如Lock.lock()或Semaphore.acquire())都是以AQS的acquire系列方法為入口的,共享類走acquireShared(),獨占類走acquire()
顯然他們下一步都是調用tryXXX方法,但是在AQS里面,這兩個方法都是直接拋異常
這也是AQS精心設計所在,既然要以AQS為基礎實現共享獨占類,那么子類就要去實現這兩個方法之一,為什么不用abstract方法呢?因為AQS要作為兩種類的基類,如果用abstract方法,那么當我實現某一功能的時候,這兩個方法都要實現,大大降低了代碼的可讀性,也毫無意義。
關于釋放,和獲取如出一轍
這里我先提綱挈領的描述了下子類實現以AQS為工具類的入口和出口方法,下面我們通過具體的工具類來分析。
ReentrantLock && ReentrantReadWriteLock(獨占類分析)
-
ReentrantLock
從獨占類開始分析,就拿ReentrantLock開刀吧,先分析公平模式(其實就是開頭有點差異,后面基本差不多),這個類也是大家最常見的類之一
lock.lock();
try {
//do something as you want
} catch (InterruptedException e) {
//deal with the Exception
}finally {
lock.unlock();
}
這幾乎及時ReentrantLock的模板使用方法,我們著重分析下lock和unlock方法到底發生了什么
我們就看看ReentrantLock自己實現的tryAcquire發生了什么
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();//如何看待處理這個state決定了這是一個什么樣的工具類
if (c == 0) { //若當前資源無人占有
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);//那么就占為己有吧
return true;//返回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;//代表資源已經被別人占有了,返回失敗
}
如果剛剛的tryAcquire成功了,那么也沒后面acquireQueued的事了,如果失敗了,那么就要進syn隊列了,acquireQueued方法我們前面也花大篇幅分析了,這就是lock方法,其實很簡單吧?
我們再看下unlock方法
protected final boolean tryRelease(int releases) {
int c = getState() - releases; //將state減去1,意為釋放一份資源
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {//如果減掉了1以后state為0說明現在沒人占有資源
free = true;
setExclusiveOwnerThread(null);//釋放當前線程
}
setState(c);
return free;//返回釋放資源成功
}
回到release方法
public final boolean release(int arg) {
if (tryRelease(arg)) {//如果釋放成功了就在A處喚醒后面等待的線程節點
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//A
return true;
}
return false;//否則不作為
}
ReentrantLock基于AQS的實現的思想就是對于state狀態,看作持有鎖的對象的數目,如果是0那么別人可以去持有這個鎖,不然就是掛起等待,釋放的時候如果state變為0,就去syn隊列喚醒等待者,不然就是什么也不做。ReentrantLock中還有可中斷的lockInterruptibly()以及帶超市的tryAcquire方法,其實本質上就是多了對線程中斷的檢測和對執行時間的檢測,在這里不重復描述了,有心的讀者可以自己在源碼中查看,和上述的方法幾乎沒什么差異,主要邏輯都是相同的。
對于ReentrantLock最后要提的就是公平非公平模式,非公平模式的性能較高,因為線程被喚醒和實際獲得執行權中間是有較大的時間差的,非公平模式可以利用這段時間。
ReentrantLock的分析也就到此為止,我們再看看ReentrantReadWriteLock
- ReentrantReadWriteLock
對于這個類有一點要解釋的及時他并沒有同時實現共享獨占的入口方法,而是內部實現了一個ReadLock和一個WriteLock,如圖所示
這個類的讀鎖是共享鎖,寫鎖是獨占鎖,共享鎖我們后文會舉例子,排它鎖和前面分析的如出一轍,這里我想講的就是這個ReentrantReadWriteLock類的鎖降級特性,這個鎖支持降級但不支持升級,至于原因也很好理解,寫鎖降級成讀鎖,大家一起進來也沒事,但是如果正在讀的多個鎖突然給升級成寫鎖,也就是由共享模式變成了獨占模式,那豈不是亂了套?到底排誰?死鎖?至于降級,我這給出官方的例子,一看就懂
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) { //如果緩存沒有失效,那么只需要讀鎖就行,可并行訪問
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();//如果失效了,那么就要用寫鎖防止多個線程同時寫
try {
//這里再檢查下,因為有可能其他線程已經先下手了
if (!cacheValid) {
data = ...
cacheValid = true;
}
// 鎖降級,先上讀鎖
rwl.readLock().lock();
} finally {//最后把寫鎖放了
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
//原則:鎖交替時,讀鎖必須先放了自己才能上寫鎖,寫鎖可以先上讀鎖后再放了自己
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
當然還有用讀寫鎖包裝數據結構的
class RWDictionary {
private final Map<String, Data> m = new TreeMap<String, Data>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public Data get(String key) {
r.lock();
try { return m.get(key); }
finally { r.unlock(); }
}
public String[] allKeys() {
r.lock();
try { return m.keySet().toArray(); }
finally { r.unlock(); }
}
public Data put(String key, Data value) {
w.lock();
try { return m.put(key, value); }
finally { w.unlock(); }
}
public void clear() {
w.lock();
try { m.clear(); }
finally { w.unlock(); }
}
}
這樣就包裝了一個支持并發的有序Map,相比于直接用Collections的同步包裝方法,這樣做能支持更大的吞吐量。
獨占類的分析我們到此為止。
Semaphore && CountDownLatch(共享類分析)
關于共享類,重點就分析這兩者,以我的理解,這兩者真的是孿生兄弟一樣,Semaphore兩個方法acquire和release,CountDownLatch兩個方法await和countdown,真的是一一對應的
Semaphore的acquire方法去查看state是否大于0,如果大于0就進入critical region執行代碼,不然就掛起,CountDownLatch的await方法檢測state是否為0,是的話走你,不是的話掛起
Semphore的release方法對state++,CountDownLatch的countdown對state--,這兩個類,在我看來,真的,肯定是同卵的。
隨便挑一個分析下吧,我們這就挑CountDownLatch分析,Semphore和操作系統里面學的的信號量是同一個東西,acquire和release就是PV操作
還是和前文分析獨占類一樣的方法,入口方法是acquireShared,然后會走tryXXX方法,最后出口是releaseShared,里面肯定有一層tryReleaseShared。我們來看看是不是把。
這里的實現非常簡單,以為CountDownLatch就是這樣一個類,只有countdown夠次數了,acquire才算成功,代碼才能走下去,不然就走doAcquireShared方法,對,又要貼那段看了好幾次的代碼了。
//這段代碼估計都看膩了吧
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//沒錯,又是這里。。沒看懂的可以看上半文的分析
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
acquire說完了,看看release吧,也很簡單,步驟我不重復,就是那幾步,我本來背不下來的,寫到這,真的,我可以倒著背了。
如果state已經是0了,也就意味著countdown次數夠了,那么就要走doReleaseShared方法了
private void doReleaseShared() {
//主要的邏輯就是依次喚醒syn隊列里面等待的線程,也就是那個await的線程
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
CountDownLatch算是講完了,開頭我就說Semaphore和他是雙胞胎,讀者可以自行比對著看,幾乎就是一樣的,對于基于AQS實現的類,還是我強調的那句話,你怎么看state,他就是個怎么樣的工具類。我用英文闡述我的觀點可能更好理解
***How you see the state makes what it is ***
不知道你們能不能理解,我盡力啦,因為水平有限,如有錯誤,希望能指出,希望能多多交流。如果大家有什么問題,源碼永遠是最好的幫手。
參考: