此篇博客所有源碼均來自JDK 1.8
在上篇博客【死磕Java併發】—–J.U.C之AQS:AQS簡介中提到了AQS內部維護着一個FIFO隊列,該隊列就是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;
}
}
CLH同步隊列結構圖如下:
入列
學了數據結構的我們,CLH隊列入列是再簡單不過了,無非就是tail指向新節點、新節點的prev指向當前最後的節點,當前最後一個節點的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;
}
}
}
}
在上面代碼中,兩個方法都是通過一個CAS方法compareAndSetTail(Node expect, Node update)來設置尾節點,該方法可以確保節點是線程安全添加的。在enq(Node node)方法中,AQS通過“死循環”的方式來保證節點可以正確添加,只有成功添加後,當前線程纔會從該方法返回,否則會一直執行下去。
過程圖如下:
出列
CLH同步隊列遵循FIFO,首節點的線程釋放同步狀態後,將會喚醒它的後繼節點(next),而後繼節點將會在獲取同步狀態成功時將自己設置爲首節點,這個過程非常簡單,head執行該節點並斷開原首節點的next和當前節點的prev即可,注意在這個過程是不需要使用CAS來保證的,因爲只有一個線程能夠成功獲取到同步狀態。過程圖如下: