天天看點

Java并發系列(1)AbstractQueuedSynchronizer源碼之概要分析

學習Java并發程式設計不得不去了解一下java.util.concurrent這個包,這個包下面有許多我們經常用到的并發工具類,例如:ReentrantLock, CountDownLatch, CyclicBarrier, Semaphore等。而這些類的底層實作都依賴于AbstractQueuedSynchronizer這個類,由此可見這個類的重要性。

是以在Java并發系列文章中我首先對AbstractQueuedSynchronizer這個類進行分析,由于這個類比較重要,而且代碼比較長,為了盡可能分析的透徹一些,我決定用四篇文章對該類進行一個比較完整的介紹。本篇文章作為概要介紹主要是讓讀者們對該類有個初步了解。為了叙述簡單,後續有些地方會用AQS代表這個類。

1. AbstractQueuedSynchronizer這個類是幹嘛的?

相信有許多讀者使用過ReentrantLock,但是卻不知道AbstractQueuedSynchronizer的存在。其實ReentrantLock實作了一個内部類Sync,該内部類繼承了AbstractQueuedSynchronizer,所有鎖機制的實作都是依賴于Sync内部類,也可以說ReentrantLock的實作就是依賴于AbstractQueuedSynchronizer類。與此類似,CountDownLatch, CyclicBarrier, Semaphore這些類也是采用同樣的方式來實作自己對于鎖的控制。

可見,AbstractQueuedSynchronizer是這些類的基石。那麼AQS内部到底實作了什麼以至于所有這些類都要依賴于它呢?可以這樣說,AQS為這些類提供了基礎設施,也就是提供了一個密碼鎖,這些類擁有了密碼鎖之後可以自己來設定密碼鎖的密碼。

此外,AQS還提供了一個排隊區,并且提供了一個線程訓導員,我們知道線程就像一個原始的野蠻人,它不懂得講禮貌,它隻會橫沖直撞,是以你得一步一步去教它,告訴它什麼時候需要去排隊了,要到哪裡去排隊,排隊前要做些什麼,排隊後要做些什麼。這些教化工作全部都由AQS幫你完成了,從它這裡教化出來的線程都變的非常文明懂禮貌,不再是原始的野蠻人,是以以後我們隻需要和這些文明的線程打交道就行了,千萬不要和原始線程有過多的接觸!

2. 為何說AbstractQueuedSynchronizer提供了一把密碼鎖?

//同步隊列的頭結點
private transient volatile Node head;

//同步隊列的尾結點
private transient volatile Node tail;

//同步狀态
private volatile int state;

//擷取同步狀态
protected final int getState() {
return state;
}

//設定同步狀态
protected final void setState(int newState) {
state = newState;
}

//以CAS方式設定同步狀态
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}           

上面的代碼列出了AQS的所有成員變量,可以看到AQS的成員變量隻有三個,分别是同步隊列頭結點引用,同步隊列尾結點引用以及同步狀态。

注意,這三個成員變量都使用了volatile關鍵字進行修飾,這就確定了多個線程對它的修改都是記憶體可見的。整個類的核心就是這個同步狀态,可以看到同步狀态其實就是一個int型的變量,大家可以把這個同步狀态看成一個密碼鎖,而且還是從房間裡面鎖起來的密碼鎖,state具體的值就相當于密碼控制着密碼鎖的開合。當然這個鎖的密碼是多少就由各個子類來規定了,例如在ReentrantLock中,state等于0表示鎖是開的,state大于0表示鎖是鎖着的,而在Semaphore中,state大于0表示鎖是開的,state等于0表示鎖是鎖着的。

2. AbstractQueuedSynchronizer的排隊區是怎樣實作的?

Java并發系列(1)AbstractQueuedSynchronizer源碼之概要分析

AbstractQueuedSynchronizer内部其實有兩個排隊區,一個是同步隊列,一個是條件隊列。從上圖可以看出,同步隊列隻有一條,而條件隊列可以有多條。同步隊列的結點分别持有前後結點的引用,而條件隊列的結點隻有一個指向後繼結點的引用。

圖中T表示線程,每個結點包含一個線程,線程在擷取鎖失敗後首先進入同步隊列排隊,而想要進入條件隊列該線程必須持有鎖才行。接下來我們看看隊列中每個結點的結構。

//同步隊列的結點
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; 
//目前結點持有的線程引用

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;
}
}

//構造器1
Node() {}

//構造器2, 預設用這個構造器
Node(Thread thread, Node mode) {
//注意持有模式是指派給nextWaiter
this.nextWaiter = mode;
this.thread = thread;
}

//構造器3, 隻在條件隊列中用到
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}           

Node代表同步隊列和條件隊列中的一個結點,它是AbstractQueuedSynchronizer的内部類。Node有很多屬性,比如持有模式,等待狀态,同步隊列中的前繼和後繼,以及條件隊列中的後繼引用等等。

可以把同步隊列和條件隊列看成是排隊區,每個結點看成是排隊區的座位,将線程看成是排隊的客人。客人剛來時會先去敲敲門,看看鎖有沒有開,如果鎖沒開它就會去排隊區領取一個号碼牌,聲明自己想要以什麼樣的方式來持有鎖,最後再到隊列的末尾進行排隊。

3. 怎樣了解獨占模式和共享模式?

前面講到每個客人在排隊前會領取一個号碼牌,聲明自己想要以什麼樣的方式來占有鎖,占有鎖的方式分為獨占模式和共享模式,那麼怎樣來了解獨占模式和共享模式呢?實在找不到什麼好的比喻,大家可以聯想一下公共廁所,獨占模式的人比較霸道,老子要麼就不進,進來了就不許别人再進了,自己一個人獨自占用整個廁所。

共享模式的人就沒那麼講究了,當它發現這個廁所已經可以用了之後,它自己進來還不算,還得熱心的問下後面的人介不介意一起用,如果後面的人不介意一起使用那就不用再排隊了大家一起上就是了, 當然如果後面的人介意那就隻好留在隊列裡繼續排隊了。

4. 怎樣了解結點的等待狀态?

我們還看到每個結點都有一個等待狀态,這個等待狀态分為CANCELLED,SIGNAL,CONDITION,PROPAGATE四種狀态。可以将這個等待狀态看作是挂在座位旁邊的牌子,辨別目前座位上的人的等待狀态。這個牌子的狀态不僅自己可以修改,其他人也可以修改。

例如當這個線程在排隊過程中已經打算放棄了,它就會将自己座位上的牌子設定為CANCELLED,這樣其他人看到了就可以将它清理出隊列。還有一種情況是,當線程在座位上要睡着之前,它怕自己睡過了頭,就會将前面位置上的牌子改為SIGNAL,因為每個人在離開隊列前都會回到自己座位上看一眼,如果看到牌子上狀态為SIGNAL,它就會去喚醒下一個人。隻有保證前面位置上的牌子為SIGNAL,目前線程才會安心的睡去。

CONDITION狀态表示該線程在條件隊列中排隊,PROPAGATE狀态提醒後面來的線程可以直接擷取鎖,這個狀态隻在共享模式用到,後面單獨講共享模式的時候會講到。

5. 結點進入同步隊列時會進行哪些操作?

//結點入隊操作, 傳回前一個結點
private Node enq(final Node node) {
for (;;) {
//擷取同步隊列尾結點引用
Node t = tail;
//如果尾結點為空說明同步隊列還沒有初始化
if (t == null) {
//初始化同步隊列
if (compareAndSetHead(new Node())) {
tail = head;
}
} else {
//1.指向目前尾結點
node.prev = t;
//2.設定目前結點為尾結點
if (compareAndSetTail(t, node)) {
//3.将舊的尾結點的後繼指向新的尾結點
t.next = node;
//for循環唯一的出口
return t;
}
}
}
}           

注意,入隊操作使用一個死循環,隻有成功将結點添加到同步隊列尾部才會傳回,傳回結果是同步隊列原先的尾結點。下圖示範了整個操作過程。

Java并發系列(1)AbstractQueuedSynchronizer源碼之概要分析

讀者需要注意添加尾結點的順序,分為三步:指向尾結點,CAS更改尾結點,将舊尾結點的後繼指向目前結點。在并發環境中這三步操作不一定能保證完成,是以在清空同步隊列所有已取消的結點這一操作中,為了尋找非取消狀态的結點,不是從前向後周遊而是從後向前周遊的。還有就是每個結點進入隊列中時它的等待狀态是為0,隻有後繼結點的線程需要挂起時才會将前面結點的等待狀态改為SIGNAL。

注:以上全部分析基于JDK1.7,不同版本間會有差異,讀者需要注意

繼續閱讀