AQS 學習
文章目錄
- AQS 學習
- lock加鎖過程
- acquire方法
- tryAcquire方法
- hasQueuedPredecessors方法
- enq方法
- compareAndSetTail方法
- addWaiter方法
- acquireQueued方法
- shouldParkAfterFailedAcquire方法
- parkAndCheckInterrupt方法
- 關於加鎖的流程圖
- unlock解鎖過程
- Condition相關
- 關於nextWaiter屬性
- 大致流程
- await方法
- 帶超時機制的await
- addConditionWaiter方法
- unlinkCancelledWaiters方法
- fullyRelease方法
- isOnSyncQueuef方法
- findNodeFromTail方法
- signal方法
- isHeldExclusively方法
- doSignal方法
- 順帶說一下doSignalAll方法
- transferForSignal方法
- checkInterruptWhileWaiting方法
- transferAfterCancelledWait方法
- reportInterruptAfterWait方法
- 共享鎖
- CountDownLatch
- CyclicBarrier
- CountDownLatch和CyclicBarrier的區別
- Semaphore
- 參考內容
AQS
就是AbstractQueuedSynchronizer
,抽象的隊列同步器,AQS實現了對同步狀態的管理,以及對阻塞線程進行排隊,等待通知等等一些底層的實現處理。AQS的核心也包括了這些方面:同步隊列,獨佔式鎖的獲取和釋放,共享鎖的獲取和釋放以及可中斷鎖,超時等待鎖獲取這些特性的實現
前置知識
//這是AQS的三個很重的變量,也可以說明等待隊列底層其實是一個鏈表
//頭節點,不保存真實信息,只表示位置,不代表實際的等待線程
private transient volatile Node head;
//尾節點
private transient volatile Node tail;
//狀態
private volatile int state;
可以看一下這個圖,加深對head和tail的理解
FairSync的繼承結構,它其實是間接繼承自AQS的
lock加鎖過程
這裏以FairSync(公平鎖)的lock方法爲例,解析一下lock過程,以及AQS中的一些基礎方法
acquire方法
//FairSync的lock方法
final void lock() {
acquire(1);
}
/*
AQS的accquire方法
1. 會先調用tryAcquire方法,這個方法是在AQS中並沒有用具體的實現,只派出了一個異常,在FairSync中重寫了這方法,
2. tryAcquire會先嚐試獲得鎖,如果返回true,也就是獲得鎖成功,導致邏輯端路,那麼結束了
3. 如果tryAcquire返回false,也就是獲得鎖失敗,就要往阻塞隊列添加當前線程,這是addWaiter方法實現的功能,具體參考下方源碼分析
4. acquireQueued方法可以對排隊中的線程進行“獲鎖”操作。
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//如果返回打斷,就要執行下面的代碼
//也就是說 獲取了鎖以後還要中斷線程
selfInterrupt();
}
tryAcquire方法
/*
這個方法的作用是嘗試獲得鎖,如果獲得成功就返回true,失敗則返回false
*/
protected final boolean tryAcquire(int acquires) {
//獲得當前線程
final Thread current = Thread.currentThread();
//state是AQS一個很重要的變量,爲0表示鎖還沒有被獲取
int c = getState();
if (c == 0) {
//如果返回了false, 就說明可以嘗試獲得鎖
if (!hasQueuedPredecessors() &&
//compareAndSetState方法就是以CAS的方式設置state,保證原子性
compareAndSetState(0, acquires)) {
//設置當前擁有獨佔訪問的線程,這個方法內部就一句話,應該不需要解釋了吧
//clusiveOwnerThread = thread;
setExclusiveOwnerThread(current);
return true;
}
}
//如果c==1,就是鎖已被獲得,那就查看獲得鎖的線程是不是當前線程
else if (current == getExclusiveOwnerThread()) {
//一般就是+1,可重入鎖的表現
int nextc = c + acquires;
//int溢出爲負數,也就是超過最大鎖數
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
//那就增加可重入鎖的參數
//另外說一句,這邊爲什麼沒有采用CAS的方式設置state,我覺得是因爲這裏滅有併發差生的問 //因爲最多隻有一個線程能夠進入到這個代碼塊
setState(nextc);
//返回true表示獲得鎖成功
return true;
}
//要麼就是c=0,但是自旋設置鎖,要麼就是鎖已被佔用
return false;
}
hasQueuedPredecessors方法
/*
判斷阻塞隊列是否有等待的線程,如果有返回true,沒有返回false
1. 如果h==t 說明還沒有初始化,h和t都是null,或者是初始化了,但是隊列中還沒有元素
2. (s = h.next) == null,這個判斷是爲了在併發情況下可能會產生的問題,詳細問題可以看下面的enq方法
3. s.thread != Thread.currentThread()說明阻塞隊列的第一個有效節點線程與當前線程不同,當前線程必須加入進等待隊列,也就是有比當前線程優先級高的線程
*/
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
enq方法
/*
入隊操作並不是一個原子操作,所以就會存在初始化完成,也就是進入else代碼塊,將node節點設置爲tail,使得head!=tail,但是t.next=node並沒有執行,也就是導致hasQueuedPredecessors中(s = h.next) == null的問題,這是需要判斷出阻塞隊列中有元素的,只是因爲在併發環境下,設置了tail指向head節點的信息,但沒有設置head指向tail指針。
這段話解釋hasQueuedPredecessors方法中的問題
——————————————————————————————————————————————————————————————————————————————————
我們由下面的分析可以知道,進入enq有兩個可能一個是沒有初始化,一個是CAS競爭失敗,都會在這個得到體現
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
//沒有初始化
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
}
/*進到這裏有兩種可能
1. 沒有初始化,那麼初始化完成之後,會同時添加node到阻塞隊列
2. CAS競爭失敗進入到這裏,那麼就會再次嘗試CAS的方式添加到隊尾,注意這是個死循環,只有當自旋 成功的時候,纔會return
*/
//addWaiter一樣的操作,不過是變量名換了一下
else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
compareAndSetTail方法
就是CAS的實現。
compareAndSetTail方法,完成尾節點的設置。這個方法主要是對tailOffset和Expect進行比較,如果tailOffset的Node和Expect的Node地址是相同的,那麼設置Tail的值爲Update的值。
addWaiter方法
/*
1. 傳入的參數mode是Node.EXCLUSIVE,是AQS中Node內部類的一個常量
static final Node EXCLUSIVE = null;
*/
private Node addWaiter(Node mode) {
// 獲得當前線程封裝成Node
Node node = new Node(Thread.currentThread(), mode);
// 獲得tail尾節點,嘗試往阻塞裏添加節點
Node pred = tail;
//如果pred也就是tail!=null,說明已經初始化過了
//PS:這下面的代碼其實和enq的那段代碼邏輯基本一樣
if (pred != null) {
//設置當前node節點的前驅
node.prev = pred;
//CAS設置tail
if (compareAndSetTail(pred, node)) {
//設置pred的後繼節點
pred.next = node;
//返回新加的節點
return node;
}
}
//到了這一步,只能說明阻塞隊列沒有初始化,或色是CAS失敗,也就是有線程在競爭這個資源
//enq方法可以回顧上面的分析
enq(node);
return node;
}
acquireQueued方法
//返回是否打斷
final boolean acquireQueued(final Node node, int arg) {
//標誌是否獲得成功,默認失敗
boolean failed = true;
try {
//標誌是否被打斷,默認沒有
boolean interrupted = false;
//死循環
for (;;) {
//獲得node節點的前驅
final Node p = node.predecessor();
//如果node的前驅p是head,說明可以嘗試去獲得鎖
if (p == head && tryAcquire(arg)) {
//進入這裏,獲得成功,設置head
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//獲得鎖失敗要麼是p是不是頭節點,沒有資格獲得鎖,要麼就是p是頭節點,但是被別的線程搶先獲得鎖了(可能是非公平鎖被搶佔了),就判斷是否要掛起線程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire方法
/*
傳進來2個參數,一個node的前驅結點pred,一個node節點
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//獲得pred的waitStatus的狀態
int ws = pred.waitStatus;
//如果是Node.SIGNAL,也就是-1,那麼就說明當前節點可以被阻塞
if (ws == Node.SIGNAL)
return true;
// >0 就是取消狀態,說明當前節點前面的節點已經取消了,就要往前找到一個沒有被取消的節點
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
//設置前一個節點爲SIGNAL狀態,下一次進來的時候就會return
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt方法
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//阻塞線程
//線程被重新喚醒後執行
return Thread.interrupted();
}
關於加鎖的流程圖
-
線程A進入,發現可以獲得鎖,那就獲得鎖,並沒有對阻塞隊列做任何處理
-
線程B進入,不能獲得鎖,那麼就要加入到阻塞隊列中
發現阻塞隊列還沒有初始化,那就先初始化,如下圖
阻塞隊列初始化完成,就將線程B加入到阻塞隊列中,同時設置前面節點的waitStatus爲-1,表示後繼結點需要 被喚醒。
- 線程C加入,不能獲得鎖,那就加入到阻塞隊列,發現阻塞隊列已經初始化,那就直接加到隊尾,比設置前驅節點的waitStatus爲-1,表示線程C需要被喚醒
unlock解鎖過程
public void unlock() {
sync.release(1);
}
release方法
public final boolean release(int arg) {
//鎖被完全釋放了,喚醒阻塞隊列中的節點
if (tryRelease(arg)) {
Node h = head;
/*
h == null Head還沒初始化。初始情況下,head == null,第一個節點入隊,Head會被初始化一個虛擬節 點。所以說,這裏如果還沒來得及入隊,就會出現head == null 的情況。
h != null && waitStatus == 0 表明後繼節點對應的線程仍在運行中,不需要喚醒。
h != null && waitStatus < 0 表明後繼節點可能被阻塞了,需要喚醒。
*/
if (h != null && h.waitStatus != 0)
//解除阻塞
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease方法
//返回true,完全釋放鎖,返回false,沒有完全釋放鎖
protected final boolean tryRelease(int releases) {
//主要是針對可重入鎖的次數
int c = getState() - releases;
//當前線程不是持有鎖的線程,拋出異常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
//因爲是可重入鎖,判斷是否完全釋放鎖
boolean free = false;
//c==0 鎖完全釋放了,打標記,設置擁有線程爲null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//設置state
setState(c);
return free;
}
unparkSuccessor方法
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
//即-1 ,那就設置爲0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
//如果下個節點是null或者下個節點被cancelled,就找到隊列最開始的非cancelled的節點
if (s == null || s.waitStatus > 0) {
s = null;
/*
倒着遍歷是因爲,在addWaiter方法中,由於是雙向鏈表,那麼必然會有1->2,2->1的過程
node.prev = pred;
pred.next = node;
上面兩句是來自於addWaiter方法,我們可以看到是先設置前驅指針,然後設置後繼指針,
也就是在併發情況下,可能存在只設置了前驅指針,當腰設置後繼的時候,線程就被切換了,導致還沒有設 置.
那麼,這裏的倒着遍歷就可以理解了,防止沒有遍歷到所有的元素.
*/
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//解除一個阻塞線程
if (s != null)
LockSupport.unpark(s.thread);
}
Condition相關
操作系統中有一個關於生產者-消費者的例子,在java中可以利用condition實現
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class BoundedBuffer {
final Lock lock = new ReentrantLock();
// condition 依賴於 lock 來產生
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
// 生產
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await(); // 隊列已滿,等待,直到 not full 才能繼續生產
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal(); // 生產成功,隊列已經 not empty 了,發個通知出去
} finally {
lock.unlock();
}
}
// 消費
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await(); // 隊列爲空,等待,直到隊列 not empty,才能繼續消費
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull .signal(); // 被我消費掉一個,隊列 not full 了,發個通知出去
return x;
} finally {
lock.unlock();
}
}
}
這其中的newCondition方法如下
final ConditionObject newCondition() {
return new ConditionObject();
}
其中ConditionObject是AQS的一個內部類,有兩個比較重要的常量
private transient Node firstWaiter;
private transient Node lastWaiter;
我們知道在AQS中存在着一個阻塞隊列(本質上是雙向鏈表),裏面存放想要獲得鎖但是還沒有獲得鎖的線程。
在我們使用condition的時候,引入一個新的隊列,叫條件隊列,這是一個單向鏈表(可以查看Node類,裏面有一個nextWaiter屬性從側面印證了這一點)。
關於nextWaiter屬性
其實nextWaiter的作用並不只是表示條件隊列,在AQS的獨佔鎖和共享鎖的模式下,用於表示這兩種不同的節點。
// 共享模式
static final Node SHARED = new Node();
// 獨佔模式
static final Node EXCLUSIVE = null;
// 其他模式
// 其他非空值:條件等待節點(調用Condition的await方法的時候)
下圖的condition1和condition2都是條件隊列。
大致流程
- 調用await,將一個線程放入條件隊列。
- 某一個線程調用signal,喚醒一個線程,從條件隊列中取出隊首的放入阻塞隊列(其實在java中稱爲Sync隊列)。
await方法
public final void await() throws InterruptedException {
//清除中斷信息,如果當前已經中斷,則拋出異常
if (Thread.interrupted())
throw new InterruptedException();
//將當前節點加入到等待隊列
Node node = addConditionWaiter();
//這個方法的作用就是完全釋放鎖,返回state次數
int savedState = fullyRelease(node);
int interruptMode = 0;
//比較重要的一個while,下面方法的分析
while (!isOnSyncQueue(node)) {
/*
掛起線程
在第一次進入的時候,isOnSyncQueue是false,表示node還在條件隊列中,那麼需要喚醒,所以從
流程上來說,只有喚醒了才能進行下面的代碼,所以下面分析signal方法
*/
LockSupport.park(this);
/*
在經過signal之後,同步隊列中被放入一個線程,這個線程總會有被重新激活的時候,如果重新激活了, 就會執行下面代碼
*/
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//之前釋放了鎖,現在要重新獲得
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
/*checkInterruptWhileWaiting中,我們判斷如果中斷髮生在signal前也會將節點加入到阻塞隊列,但這個是 沒有設置nextWaiter=null的,在doSignal中是設置了的
*/
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
帶超時機制的await
/*
加入了time,引入了超時機制,意思是如果超時了,就會主動加入到阻塞隊列
*/
public final boolean await(long time, TimeUnit unit)
throws InterruptedException {
long nanosTimeout = unit.toNanos(time);
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
final long deadline = System.nanoTime() + nanosTimeout;
boolean timedout = false;
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
if (nanosTimeout <= 0L) {
timedout = transferAfterCancelledWait(node);
break;
}
if (nanosTimeout >= spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
nanosTimeout = deadline - System.nanoTime();
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
return !timedout;
}
addConditionWaiter方法
private Node addConditionWaiter() {
Node t = lastWaiter;
/*
如果t==null 說明條件隊列還沒有元素
t.waitStatus != Node.CONDITION,可能存在cancel的節點,那就要清理cancel節點
*/
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
//lastWaiter可能有變化,重新獲得
t = lastWaiter;
}
//封裝當前線程節點
Node node = new Node(Thread.currentThread(), Node.CONDITION);
//t==null 說明條件隊列還沒有元素,那就設置第一個節點元素
if (t == null)
firstWaiter = node;
//否則就在尾部接一個上去
else
t.nextWaiter = node;
//更新尾節點
lastWaiter = node;
return node;
}
unlinkCancelledWaiters方法
//這個就是清理無效節點的操作,就是一個普通鏈表的刪除操作
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
fullyRelease方法
final int fullyRelease(Node node) {
//標記是否釋放失敗,默認爲失敗
boolean failed = true;
try {
//獲得state的值
int savedState = getState();
//一次性釋放,不再每次減1,這次是直接嘗試減state
if (release(savedState)) {
failed = false;
return savedState;
}
//如果當前是沒有獲得鎖的,就拋出異常
else {
throw new IllegalMonitorStateException();
}
} finally {
//設置節點狀態時CANCELLED
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
isOnSyncQueuef方法
/*
這個方法是判斷node是否在同步隊列(也就是我一直說的阻塞隊列)
*/
final boolean isOnSyncQueue(Node node) {
/*node.waitStatus == Node.CONDITION ,很明顯還是條件隊列中節點
node.prev == null,pre屬性是阻塞隊列的一個屬性,我們在前面說過,將一個節點加入到阻塞隊列這個雙 向鏈表,是個非原子操作,是先設置prev,再設置next,所以只需要判斷prev是否爲null,也能判斷是否進入阻 塞隊列
*/
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
//同上面所說,如果next都有值了,那麼就進入阻塞隊列了
if (node.next != null)
return true;
return findNodeFromTail(node);
}
findNodeFromTail方法
//這個方法就是從尾節點開始遍歷阻塞隊列,判斷node是否在阻塞隊列中
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
signal方法
public final void signal() {
//要調用signal就肯定要持有鎖纔行,就在這裏判斷,如果沒有鎖,就拋出異常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
//如果firstWaiter==null,也就是條件隊列沒有內容,自然不用喚醒
if (first != null)
doSignal(first);
}
isHeldExclusively方法
//方法很簡單,就是判斷當前線程是不是持有鎖的線程
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
doSignal方法
private void doSignal(Node first) {
do {
//如果當前條件隊列只有一個元素,那就清空
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
//斷開節點
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
順帶說一下doSignalAll方法
//可以看到與doSignal方法最大的區別就是doSignalAll是將所有能夠加入到阻塞隊列中的節點都加進去
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
transferForSignal方法
final boolean transferForSignal(Node node) {
//如果將當前節點從CONDITION設置爲0失敗,那麼就會返回while語句,繼續尋找一個新的節點喚醒,也就是說如果失敗,那麼就放棄這個節點
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//進入到這裏說明說明已經設置條件隊列中的第一個元素的status是0,那麼就把這個節點後加入到阻塞隊列
//注意,返回的p是尾節點的前驅節點,並不是尾節點
Node p = enq(node);
int ws = p.waitStatus;
//大於0就說明是CACAELLED,或者就需要把waitStatus設置爲SIGNAL,以方便在阻塞隊列中的喚醒
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
//喚醒當前線程
LockSupport.unpark(node.thread);
return true;
}
關於這段代碼的 LockSupport.unpark(node.thread);
多說一句。
這邊的兩個判斷條件一定概率上處於優化作用,因爲就算不執行,那麼在signal之後,也會將一個等待隊列中的節點加入到阻塞隊列。在阻塞隊列中的節點同樣有希望解除掛起狀態,重新獲得鎖,繼續執行await中剩下的代碼。這裏就是直接喚醒,減少了再去阻塞隊列中激活線程的一步。
checkInterruptWhileWaiting方法
//判斷是否被中斷
/*
三種不同的返回值
REINTERRUPT: 代表 await 返回的時候,需要重新設置中斷狀態
THROW_IE: 代表 await 返回的時候,需要拋出 InterruptedException 異常
0 : 說明在 await 期間,沒有發生中斷
*/
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
transferAfterCancelledWait方法
final boolean transferAfterCancelledWait(Node node) {
//如果設置成功,說明中斷是在signal前發生的,因爲signal會將waitStatus設爲0
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
//也會將節點加入到阻塞隊列
enq(node);
return true;
}
//要等到node加入到阻塞隊列,才產生返回
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
transferAfterCancelledWait方法並不在ConditionObject中定義,而是由AQS提供。這個方法根據是否中斷髮生時,是否有signal操作來“摻和”來返回結果。方法調用CAS操作將node的waitStatus從CONDITION設置爲0,如果成功,說明當中斷髮生時,說明沒有signal發生(signal的第一步是將node的waitStatus設置爲0),在調用enq將線程放入Sync隊列後直接返回true,表示中斷先於signal發生,即中斷在await等待過程中發生,根據await的語義,在遇到中斷時需要拋出中斷異常,返回true告訴上層方法返回THROW_IT,後續會根據這個返回值做拋出中斷異常的處理。
如果CAS操作失敗,是否說明中斷後於signal發生呢?只能說這時候我們不能確定中斷和signal到底誰先發生,只是在我們做CAS操作的時候,他們倆已經都發生了(中斷->interrupted檢測->signal->CAS,或者signal->中斷->interrupted檢測->CAS都有可能),這時候我們無法判斷到底順序是怎樣,這裏的處理是不管怎樣都返回false告訴上層方法返回REINTERRUPT,當做是signal先發生(線程被signal喚醒)來處理,後續根據這個返回值做“補上”中斷的處理。在返回false之前,我們要先做一下等待,直到當前線程被成功放入Sync鎖等待隊列。
源自:https://www.cnblogs.com/go2sea/p/5630355.html
reportInterruptAfterWait方法
//按照之前定義的返回,選擇拋異常還是處理中斷
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
共享鎖
上面的關於ReentrantLock的內容和condition都是關於獨佔鎖的。AQS中還存在着共享鎖,下面會簡單分析一下共享鎖。
先說明一下共享鎖的特點(個人看法)
- 一開始我有點無法理解共享鎖,但是可以類比概念上的讀寫鎖,這樣就好理解了。我們知道讀鎖是可以多次訪問的,也就是說是個共享鎖。**注意:state的值不會改變。**那麼AQS判斷是否可以擁有共享鎖是通過tryAcquireShared方法來判斷的,如果>=0,那麼久允許獲取。在CountDownLatch中。這個方法一個簡單的實現就是直接判斷state是不是爲0。我們可以想象只有讀鎖的環境下,state的值永遠是1,那麼也就對應了任何讀操作都能獲得鎖。
下面簡單分析一下共享鎖,這裏面的代碼寫的非常巧妙,有很多不同判斷,有點難以理解。
共享鎖的內容其實和獨佔鎖非常類似,可以從方法名上就可以看出。
獨佔鎖 | 共享鎖 |
---|---|
tryAcquire(int arg) | tryAcquireShared(int arg) |
tryAcquireNanos(int arg, long nanosTimeout) | tryAcquireSharedNanos(int arg, long nanosTimeout) |
acquire(int arg) | acquireShared(int arg) |
acquireQueued(final Node node, int arg) | doAcquireShared(int arg) |
acquireInterruptibly(int arg) | acquireSharedInterruptibly(int arg) |
doAcquireInterruptibly(int arg) | doAcquireSharedInterruptibly(int arg) |
doAcquireNanos(int arg, long nanosTimeout) | doAcquireSharedNanos(int arg, long nanosTimeout) |
release(int arg) | releaseShared(int arg) |
tryRelease(int arg) | tryReleaseShared(int arg) |
------------- | doReleaseShared() |
acquireShared方法
//獲得鎖
public final void acquireShared(int arg) {
/* tryAcquireShared在AQS中並沒有具體的實現,只是拋出了一個異常,在CountDownLatch,Semaphore, ReentrantReadWriteLock中有實現,部分類後面後介紹
說明一下tryAcquireShared的返回值
如果該值小於0,則代表當前線程獲取共享鎖失敗
如果該值大於0,則代表當前線程獲取共享鎖成功,並且接下來其他線程嘗試獲取共享鎖的行爲很可能成功
如果該值等於0,則代表當前線程獲取共享鎖成功,但是接下來其他線程嘗試獲取共享鎖的行爲會失敗
*/
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
doAcquireShared方法
/*
這個方法的作用就是獲取共享鎖
*/
private void doAcquireShared(int arg) {
//與獨享鎖的第一個不同,Node.SHARED和Node.EXCLUSIVE的區別,本質上是區分兩種模式
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
//第二個不同
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
//這個方法很重要
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
setHeadAndPropagate方法
/*
共享鎖的特點就是能有多個線程擁有鎖,所以如果你當前線程能夠擁有鎖,那麼就需要喚醒阻塞隊列中同同樣能獲得鎖的系節點
*/
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
//只有這個地方會修改head,與doReleaseShared中的跳出循環條件有關聯
setHead(node);
/*
1.propagate > 0 表示調用方指明瞭後繼節點需要被喚醒
2.頭節點後面的節點需要被喚醒(waitStatus<0),不論是老的頭結點還是新的頭結點
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//如果當前節點的後繼節點是共享類型或者沒有後繼節點,則進行喚醒
//這裏可以理解爲除非明確指明不需要喚醒(後繼等待節點是獨佔類型),否則都要喚醒
if (s == null || s.isShared())
doReleaseShared();
}
}
doReleaseShared方法
/*
真正的喚醒線程的地方
這個方法有兩個地方會運行
1. acquireShared -> doAcquireShared -> setHeadAndPropagate 也就是獲得鎖的時候,會將阻塞隊列中同樣有機會的線程喚醒
2.releaseShared 釋放鎖的時候,同樣會喚醒其他所有能夠獲得鎖的線程
*/
private void doReleaseShared() {
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);
}
/*
關於這個,太細了
我們可以看到這個語句由兩部分
1. 判斷ws是否等於0,等於0有2種情況,head=tail下ws是0,但這不可能
那就只可能是head後面剛添加節點,本該調用shouldParkAfterFailedAcquire修改head 的ws爲-1,但是還沒有執行的時候,會出現短暫的ws==0
2. 設置ws爲Node.PROPAGATE,只有失敗了纔會continue,那麼什麼時候會失敗?
唯一的解釋就是後面的節點剛好執行了本該調用shouldParkAfterFailedAcquire修改 head的ws爲-1
這種真的是隻有極端的併發情況下才會產生的結果
*/
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
/*
這個是判斷當前的head節點有沒有改變,只有setHeadAndPropagate中才會改變,如果沒有改變就會返回
這邊可以理解爲一個優化,因爲我們在上面已經喚醒了一個線程,那麼本該可以返回,因爲已經喚醒了一個線 程,那麼喚醒第二個線程可以不用再操心了,由新喚醒的節點接替喚醒下一個節點的任務,但是這裏判斷了 head,那麼如果新喚醒的線程已經變成了新的head節點,那麼就會導致再次循環,也就是會有多個線程一起執 行喚醒線程操作,加快了喚醒速度。
*/if (h == head) // loop if head changed
break;
}
}
releaseShared方法
//這個方法就是釋放鎖的方法,具體的內部的兩個方面上面都討論過了
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
共享鎖和獨佔鎖的區別
- 當AQS的子類實現獨佔功能時,如ReentrantLock,資源是否可以被訪問被定義爲:只要AQS的state變量不爲0,並且持有鎖的線程不是當前線程,那麼代表資源不可訪問。
- 當AQS的子類實現共享功能時,如CountDownLatch,資源是否可以被訪問被定義爲:只要AQS的state變量不爲0,那麼代表資源不可以爲訪問。
CountDownLatch
CountDownLatch是一個同步工具類,用來協調多個線程之間的同步,或者說起到線程之間的通信。
CountDownLatch 這個類是比較典型的 AQS 的共享模式的使用,下面會對這個類具體的說明,如果看懂了上面的共享鎖,CountDownLatch應該還是比較好理解的。
具體例子
class Driver2 { // ...
void main() throws InterruptedException {
//創建
CountDownLatch doneSignal = new CountDownLatch(N);
//使用線程池
Executor e = Executors.newFixedThreadPool(8);
// 創建 N 個任務,提交給線程池來執行
for (int i = 0; i < N; ++i) // create and start threads
e.execute(new WorkerRunnable(doneSignal, i));
// 等待所有的任務完成,這個方法纔會返回
doneSignal.await(); // wait for all to finish
}
}
class WorkerRunnable implements Runnable {
private final CountDownLatch doneSignal;
private final int i;
WorkerRunnable(CountDownLatch doneSignal, int i) {
this.doneSignal = doneSignal;
this.i = i;
}
public void run() {
try {
doWork(i);
// 這個線程的任務完成了,調用 countDown 方法
doneSignal.countDown();
} catch (InterruptedException ex) {
} // return;
}
void doWork() { ...}
}
這個類的核心方法就兩個countDown和await
public void countDown() {
sync.releaseShared(1);
}
/*****************************************/
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
//上面分析過了
doReleaseShared();
return true;
}
return false;
}
/*
重寫了tryReleaseShared可以看到每次countDown就state--
直到state減爲0,就會喚醒所有await的線程
*/
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
/*******************這些代碼前面都已經分析過了**************************/
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
//doAcquireSharedInterruptibly就是doAcquireShared方法,只不過處理了中斷
doAcquireSharedInterruptibly(arg);
}
最後再看一下構造函數
//傳入了一個count,也就是需要執行count次countDown之後,纔會喚醒
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
CyclicBarrier
CyclicBarrier是一個同步輔助類,允許一組線程互相等待,直到到達某個公共屏障點 (下面用柵欄代替)。因爲該 barrier 在釋放等待線程後可以重用,所以稱它爲循環 的 barrier。
CyclicBarrier和CountDownLatch的實現不同,主要是通過ReentrantLock和Condition條件隊列實現的。對於這兩個概念不懂的,可以看上面的介紹。
一個例子
package thread;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
* 模擬運動員
**/
public class MyThread extends Thread {
private CyclicBarrier cyclicBarrier;
private String name;
public MyThread(CyclicBarrier cyclicBarrier, String name) {
super();
this.cyclicBarrier = cyclicBarrier;
this.name = name;
}
@Override
public void run() {
System.out.println(name + "開始準備");
try {
Thread.currentThread().sleep(5000);
System.out.println(name + "準備完畢!等待發令槍");
try {
cyclicBarrier.await();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//測試類
public class Test {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.out.println("發令槍響了,跑!");
}
});
for (int i = 0; i < 5; i++) {
new MyThread(barrier, "運動員" + i + "號").start();
}
}
}
介紹一下屬性
//設置是否打破柵欄
private static class Generation {
boolean broken = false;
}
//獨佔鎖
private final ReentrantLock lock = new ReentrantLock();
//條件隊列
private final Condition trip = lock.newCondition();
//設置同有parties個線程要同步
private final int parties;
//到達柵欄後要執行的操作
private final Runnable barrierCommand;
//利用這個實現重複利用
private Generation generation = new Generation();
//還有count個未達到柵欄
private int count;
主要方法是await,這裏面有兩個,一個是帶超時參數的,一個是不帶超時參數的
//區別就是傳入dowait的參數不同
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
/***********************************************************************/
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
breakBarrier方法
private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();
}
nextGeneration方法
/*
這裏主要做了3件事
1. 喚醒所有的線程
2. 重置count爲所有線程的值,方便開啓下一個generation
3. 再次初始化generation,這麼做的原因是區分不同的generation
*/private void nextGeneration() {
trip.signalAll();
count = parties;
generation = new Generation();
}
核心方法dowait
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//獲得當前generation
final Generation g = generation;
//如果柵欄被打破,拋出異常
if (g.broken)
throw new BrokenBarrierException();
//如果有中斷,打破柵欄,拋出異常
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
//說明有一個線程到達柵欄,那就count減1
int index = --count;
//index == 0說明所有的線程都已經到達柵欄了,那麼就可以喚醒所有的線程,執行任務了
if (index == 0) {
//判斷是否執行任務成功
boolean ranAction = false;
try {
//獲得任務
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
//開啓新的generation
nextGeneration();
return 0;
} finally {
//如果ranAction = false,說明在執行command發生了問題,那就要打破柵欄,下面會拋出異常
if (!ranAction)
breakBarrier();
}
}
//如果index!=0 ,說明還有線程未到達柵欄,就會進入這個死循環
for (;;) {
try {
//如果沒有超時標誌,那就普通的await
if (!timed)
trip.await();
//如果帶超時標誌,那就調用超時機制的await
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
//進入到這裏說明線程已經被重新喚醒,發現是被打斷喚醒的,那就打破柵欄,重新拋出異常
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
//不然就設置打段標誌
Thread.currentThread().interrupt();
}
}
//柵欄被打破,拋出異常
if (g.broken)
throw new BrokenBarrierException();
//這個方法在我看來就是提供一個正常返回的途徑,因爲在使用中,好像並不會接收這個返回值
//因爲已經已經開啓了一個新的generation,那就可以返回了
if (g != generation)
return index;
//超時,同樣拋出異常
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {a
lock.unlock();
}
}
然後對於這個方法,打破柵欄一共有3種可能
- 中斷,某個等待的線程發生了中斷,那麼會打破柵欄,同時拋出 InterruptedException 異常;
- 超時,打破柵欄,同時拋出 TimeoutException 異常;
- 指定執行的操作拋出了異常,這個我們前面也說過。
CountDownLatch和CyclicBarrier的區別
CountDownLatch: 一個或者多個線程,等待其他多個線程完成某件事情之後才能執行。
CyclicBarrier : 多個線程互相等待,直到到達同一個同步點,再繼續一起執行。
應用場景
比如說跑步比賽,有5個運動員
在出發前,只有5個運動員都準備好了,纔開始比賽,這就是CyclicBarrier,5個線程互相等待。
當比賽開始後,只有5個運動員都到達終點了,才能進行頒獎,這就是CountDownLatch,頒獎線程需要等待5個運動員線程。
Semaphore
Semaphore也叫信號量,在JDK1.5被引入,可以用來控制同時訪問特定資源的線程數量,通過協調各個線程,以保證合理的使用資源。
Semaphore內部維護了一組虛擬的許可,許可的數量可以通過構造函數的參數指定。
在我的理解中,Semaphore有點像線程池,它規定最大的資源數量。每次acquire就會減少資源數量,release會增加資源數量。
Semaphore是基於AQS共享鎖的一種實現,同時也有像ReentrantLock一樣的公平鎖和非公平鎖。
//公平策略
protected int tryAcquireShared(int acquires) {
for (;;) {
//區別就在這,公平策略先判斷阻塞隊列是否有元素,如果有,就會直接進入阻塞隊列,準備掛起線程
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
//非公平策略
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
其他感覺也沒什麼好分析的了,如果看懂了前面所有,那麼Semaphore就很容易理解,可以自己去閱讀。
參考內容
一行一行源碼分析清楚AbstractQue uedSynchronizer
一行一行源碼分析清楚AbstractQue uedSynchronizer(二)
一行一行源碼分析清楚AbstractQue uedSynchronizer(三)