前言:
上一篇文章已經講完了大致瞭解同步器對於我們的作用,這裏就來分析下如何完成線程同步。主要包括:同步隊列、獨佔式同步狀態獲取與釋放、共享式同步狀態獲取與釋放以及超時獲取同步狀態等同步器的核心數據結構與模版方法。
同步隊列
同步器依賴內部的同步隊列(一個FIFO的雙向隊列)來完成同步狀態的管理。同步器包含着兩個節點類型的引用,一個指向頭節點、一個指向尾節點。
當前線程獲取同步狀態失敗時,同步器會將當前線程及等待狀態信息構造成一個節點,將其插入同步隊列的尾部(基於CAS的設置尾節點方法),同時會阻塞該線程。
首節點時獲取同步狀態成功的節點,當首節點的線程同步狀態釋放時,會把首節點中的後繼節點中的線程喚醒,使其再次嘗試獲取同步狀態。而後繼節點在獲取同步狀態成功時將自己設置爲首節點(由於只有一個線程獲取同步狀態,所以這裏就不需要CAS)。
獨佔式同步狀態獲取與釋放
獲取同步狀態
acquire()
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
1.調用自定義同步器實現的tryAcquire方法,獲取同步狀態
2.若獲取同步狀態不成功則調用addWaiter方法將該節點加到同步隊列的尾部,最後調用acquireQueued()使該節點自旋獲取同步狀態。
3.如果線程在等待過程中被中斷過,它是不響應的。只是獲取資源後纔再進行自我中斷selfInterrupt(),將中斷補上。
addWaiter() &enq()
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;
}
//enq
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
addWaiter方法通過compareAndSetTail()方法CAS把節點加入到等待隊列的隊尾,成功並返回當前線程所在的結點。否則,通過enq(node)方法初始化一個等待隊列
enq(node)
用於將當前節點插入等待隊列,如果隊列爲空,則初始化當前隊列。整個過程以CAS自旋的方式進行,直到成功加入隊尾爲止。
acquireQueued()
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;
}
//是否應該被park
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
隊列中的線程自旋地以獨佔且不可中斷的方式獲取同步狀態(acquire),直到拿到鎖之後再返回。該方法的實現分成兩部分:
如果當前節點已經成爲頭結點,嘗試獲取鎖(tryAcquire)成功,然後返回;
否則檢查當前節點是否應該被park,然後將該線程park並且檢查當前線程是否被可以被中斷。
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;
}
這裏需要彌補一下一些知識點:Node節點的屬性----waitStatus(int)
-
CANCELLED:1,在同步隊列中等待的線程等待超時或者中斷,需要從同步隊列中取消等待。
-
SIGNAL:-1,後繼節點的線程處於等待,而當前節點的線程如果釋放同步狀態或者取消,將會通知後繼節點,表示後繼結點在等待當前結點喚醒。後繼結點入隊時,會將前繼結點的狀態更新爲SIGNAL。
-
CONDITION:-2,節點在等待隊列中,節點線程在等待condition上,當其他線程對Condition調用signal()後,會把該節點從等待隊列轉移到同步隊列中,加入到對同步狀態的獲取中
-
PROPAGATE:-3,表示下一次共享式同步狀態獲取將會無條件地被傳播下去
-
INITIAL:0,初始化狀態
shouldParkAfterFailedAcquire()對當前節點的前一個節點的狀態進行判斷,對當前節點做出不同的操作.
1、例如if (ws == Node.SIGNAL),前節點的waitStatus是Node.SIGNAL,表明前置節點已經知道了釋放同步後會通知後繼節點,,這時候當前節點應當停止自旋parkAndCheckInterrupt()
2、如果ws>0代表示是CANCELLED,這類節點直接清除出同步隊列。
3、else剩下的就是前置節點還不是Node.SIGNAL,就是不知道釋放同步狀態後還要通知後置節點,則會嘗試將前置節點狀態位修改爲Node.SIGNAL。
parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
該方法讓線程去休息,真正進入等待狀態。park()會讓當前線程進入waiting狀態。在此狀態下,有兩種途徑可以喚醒該線程:1)被unpark();2)被interrupt()。需要注意的是,Thread.interrupted()會清除當前線程的中斷標記位。
我們再回到acquireQueued(),總結下該函數的具體流程:
- 結點進入隊尾後,檢查狀態,讓前置節點知道後面還有節點,讓其釋放同步狀態的時候通知自己;
- 調用park()進入waiting狀態,等待unpark()或interrupt()喚醒自己;
- 被喚醒後,看自己是不是有資格能拿到號。如果拿到,head指向當前結點,並返回從入隊到拿到號的整個過程中是否被中斷過;如果沒拿到,繼續流程1。
最後,總結一下acquire()的流程:
- 調用自定義同步器的tryAcquire()嘗試直接去獲取資源,如果成功則直接返回;
- 沒成功,則addWaiter()將該線程加入等待隊列的尾部,並標記爲獨佔模式;
- acquireQueued()使線程在等待隊列中休息,有機會時(輪到自己,會被unpark())會去嘗試獲取資源。獲取到資源後才返回。如果在整個等待過程中被中斷過,則返回true,否則返回false。
- 如果線程在等待過程中被中斷過,它是不響應的。只是獲取資源後纔再進行自我中斷selfInterrupt(),將中斷補上。
釋放同步狀態
release()
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
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);
}
通過喚醒後繼節點,unparkSuccessor()通過unpark來喚醒處於等待狀態的線程。
小結:
在獲取同步狀態的時候,同步器維護一個同步隊列,獲取同步狀態失敗的線程都會被加入到隊列中進行自旋(並不是一直自旋,而是會停下線程進去wait狀態),移除隊列的條件是前置節點爲頭節點並且成功獲取到了同步狀態,在釋放同步狀態的時候調用tryRelease()方法釋放同步狀態,然後喚醒頭節點的後置節點。
共享式同步狀態獲取與釋放
上圖基本已經讓我們瞭解了基本的共享式鎖與獨佔式鎖之間的關係了。最典型的例子就是:當有多個線程讀寫文件時,讀操作和寫操作會發生衝突現象,寫操作和寫操作會發生衝突現象,但是讀操作和讀操作不會發生衝突現象。
acquireShared
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
這裏tryAcquireShared()依然需要自定義同步器去實現。但是AQS已經把其返回值的語義定義好了:負值代表獲取失敗;0代表獲取成功,但沒有剩餘資源;正數表示獲取成功,還有剩餘資源,其他線程還可以去獲取。所以這裏acquireShared()的流程就是:
- tryAcquireShared()嘗試獲取資源,成功則直接返回;
- 失敗則通過doAcquireShared()進入等待隊列,直到獲取到資源爲止才返回。
doAcquireShared()
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
//如果到head的下一個,因爲head是拿到資源的線程,此時node被喚醒,很可能是head用完資源來喚醒自己的
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//判斷狀態,尋找安全點,進入waiting狀態,等着被unpark()或interrupt()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
此方法用於將當前線程加入等待隊列尾部休息,直到其他線程釋放資源喚醒自己,自己成功拿到相應量的資源後才返回。
跟獨佔式其實差不多,不一樣的就在於通過tryAcquireShared大於0,來判斷是否有資源。
setHeadAndPropagate()
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);//head指向自己
//如果還有剩餘量,繼續喚醒下一個鄰居線程
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
此方法在setHead()的基礎上多了一步,就是自己甦醒的同時,如果條件符合(比如還有剩餘資源),還會去喚醒後繼結點,畢竟是共享模式!
releaseShared()
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {//嘗試釋放資源
doReleaseShared();//喚醒後繼結點
return true;
}
return false;
}
先釋放掉資源後,喚醒後繼。