文章目錄
AQS介紹
AQS隊列同步器,java.util.concurrent包中很多類都依賴於這個類所提供的隊列式的同步器,比如說常用的ReentranLock,Semaphore和CountDownLatch等
AQS詳解可以查看博客:AQS詳解
ReentranLock示例
ReentranLock和Synchronized功能類似,但是Synchronized的阻塞無法被中斷,而ReentranLock則提供了可中斷的阻塞。ReentranLock最常用的就是如下方法:
ReentrantLock lock = new ReentrantLock();
lock.lock();
lock.unlock();
公平鎖和非公平鎖
ReentranLock分爲公平鎖和非公平鎖。二者的區別就在獲取鎖是否和排隊順序相關。
如果當前鎖被另一個線程持有,那麼當前申請鎖的線程會被掛起等待,然後加入一個等待隊列裏。
理論上,先調用lock函數被掛起等待的線程應該排在等待隊列的前端,後調用的就排在後邊。如果此時,鎖被釋放,需要通知等待線程再次嘗試獲取鎖,公平鎖會讓最先進入隊列的線程獲得鎖,而非公平鎖則會喚醒所有線程,讓它們再次嘗試獲取鎖,所以可能會導致後來的線程先獲得了鎖,則就是非公平。
ReentranLock的構造函數中可以傳入一個boolean變量,確定是否適用公平鎖
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
FairSync和NonfairSync都繼承了Sync類,而Sync的父類就是AbstractQueuedSynchronizer
lock操作
ReentranLock的lock函數如下所示,直接調用了sync的lock函數,也就是調用了FairSync的lock函數:
//ReentranLock
public void lock() {
sync.lock();
}
//FairSync
final void lock() {
acquire(1);//調用了AQS的acquire函數
}
//NonfairSync
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
acquire函數可以理解爲獲取一個同一時間只能有一個函數獲取的量,這個量就是鎖概念的抽象化,acquire源碼如下:
public final void acquire(int arg) {
//tryAcquire先嚐試獲取"鎖",如果成功,直接返回,失敗繼續執行後續代碼
//addWaiter是給當前線程創建一個節點,並將其加入等待隊列
//acquireQueued是當線程已經加入等待隊列之後的行爲
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
}
tryAcquire、addWaiter、acquireQueued三個函數非常重要,這裏分別進行講述
tryAcquire源碼
//AQS類中的變量
private volatile int state;
//這是FairSync的實現,AQS中未實現,子類按照自己的需要實現該類
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//獲取AQS中的state變量,代表抽象概念的鎖
int c = getState();
if (c == 0) {
//值爲0,那麼當前獨佔性變量還未被線程佔有
if (!hasQueuedPredecessors() && //如果當前阻塞隊列上沒有先來的線程在等待,UnfairSync這裏的實現就不一致
compareAndSetState(0, acquires)) {
//成功cas,那麼代表當前線程獲得該變量的所有權,也就是說成功獲得鎖
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
//如果該線程已經獲取了獨佔性變量的所有權,那麼根據重入性原理,將state值進行加1,表示多次lock
//由於已經獲得鎖,該段代碼只會被一個線程同時執行,所以不需要進行任何並行處理
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//上述情況都不符合,說明獲取鎖失敗,返回false
return false;
}
由上述代碼我們可以發現,tryAcquire就是嘗試獲取那個線程獨佔的變量state。
state的值表示其狀態:
- 如果是0,那麼當前還沒有線程獨佔此變量
- 否則就是已經有線程獨佔了這個變量,也就是代表已經有線程獲得了鎖。但是這個時候要再進行一次判斷,看是否是當前線程自己獲得的這個鎖,如果是,那麼就增加state的值。
注意:
- compareAndSetState函數,這是使用CAS操作來設置state的值,而且state值設置了volatile修飾符,通過這兩點來確保修改state的值不會出現多線程問題
- 如果tryAcquire返回true,那麼就是獲取鎖成功;如果返回false,那麼就是未獲得鎖,需要加入阻塞等待隊列
addWaiter源碼
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
//先使用快速入列法來嘗試一下,如果失敗,則進行更加完備的入列算法
Node pred = tail;//列尾指針
if (pred != null) {
node.prev = pred; //步驟1:該節點的前趨指針指向tail
if (compareAndSetTail(pred, node)){ //步驟二:cas將尾指針指向該節點
pred.next = node;//步驟三:如果成功,讓舊列尾節點的next指針指向該節點
return node;
}
}
//cas失敗,或在pred == null時調用enq
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) { //cas無鎖算法的標準for循環,不停的嘗試
Node t = tail;
if (t == null) { //初始化
if (compareAndSetHead(new Node())) //需要注意的是head是一個哨兵的作用,並不代表某個要獲取鎖的線程節點
tail = head;
} else {
//和addWaiter中一致,不過有了外側的無限循環,不停的嘗試,自旋鎖
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
通過調用addWaiter函數,AQS將當前線程加入到了等待隊列,但是還沒有阻塞當前線程的執行,接下來分析一下acquireQueued函數
acquireQueued源碼
由於進入阻塞狀態的操作會降低執行效率,所以AQS會盡力避免試圖獲取獨佔性變量的線程進入阻塞狀態。
所以,當線程加入等待隊列之後,acquireQueued會執行一個for循環,每次都判斷當前節點是否應該獲得這個變量(在隊首了)。
如果不應該獲取或在再次嘗試獲取失敗,那麼就調用shouldParkAfterFailedAcquire判斷是否應該進入阻塞狀態,如果當前節點之前的節點已經進入阻塞狀態了,那麼就可以判定當前節點不可能獲取到鎖,爲了防止CPU不停的執行for循環,消耗CPU資源,調用parkAndCheckInterrupt函數來進入阻塞狀態。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { //一直執行,直到獲取鎖,返回
final Node p = node.predecessor();
//node的前驅如果是head,就說明node是將要獲取鎖的下一個節點
if (p == head && tryAcquire(arg)) { //所以再次嘗試獲取獨佔性變量
setHead(node); //如果成功,那麼就將自己設置爲head
p.next = null; //help GC
failed = false;
return interrupted;//此時,還沒有進入阻塞狀態,所以直接返回false,表示不需要中斷
}
//判斷是否要進入阻塞狀態
//如果shouldParkAfterFailedAcquire返回true表示需要進入阻塞,調用parkAndCheckInterrupt,
//否則表示還可以再次嘗試獲取鎖,繼續進行for循環
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//調用parkAndCheckInterrupt進行阻塞,然後返回是否爲中斷狀態
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) //前一個節點在等待獨佔性變量釋放的通知,所以,當前節點可以阻塞
return true;
if (ws > 0) { //前一個節點處於取消獲取獨佔性變量的狀態,所以,可以跳過去
//返回false
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//將上一個節點的狀態設置爲signal,返回false,
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //將AQS對象自己傳入
return Thread.interrupted();
}
阻塞和中斷
由上述分析可知,AQS通過調用LockSupport的park方法來執行阻塞當前進程的操作。其實,這裏的阻塞就是線程不再執行的含義。通過調用這個函數,線程進入阻塞狀態,上述的lock操作也就阻塞了,等待中斷或獨佔性變量被釋放
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);//設置阻塞對象,用來記錄線程被誰阻塞的,用於線程監控和分析工具來定位
UNSAFE.park(false, 0L);//讓當前線程不再被線程調度,就是當前線程不再執行.
setBlocker(t, null);
}
unlock操作
與lock操作類似,unlock操作調用了AQS的relase方法,參數和調用acquire時一樣,都是1
public final boolean release(int arg) {
if (tryRelease(arg)) { //釋放獨佔性變量,起始就是將status的值減1,因爲acquire時是加1
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//喚醒head的後繼節點
return true;
}
return false;
}
由上述代碼可知,release就是先調用tryRelease來釋放獨佔性變量。如果成功,那麼就看一下是否有等待鎖的阻塞線程,如果有,就調用unparkSuccessor來喚醒他們
protected final boolean tryRelease(int releases) {
//由於只有一個線程可以獲得獨佔先變量,所以,所有操作不需要考慮多線程
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { //如果等於0,那麼說明鎖應該被釋放了,否在表示當前線程有多次lock操作.
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
tryRelease中的邏輯也體現了可重入鎖的概念,只有等到state的值爲1時,才代表鎖真正被釋放了。所以獨佔性變量state的值就代表鎖的有無。當state=0時,表示鎖未被佔有,否則表示當前鎖已經被佔有
private void unparkSuccessor(Node node) {
//一般來說,需要喚醒的線程就是head的下一個節點,但是如果它獲取鎖的操作被取消,或在節點爲null時
//就直接繼續往後遍歷,找到第一個未取消的後繼節點
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);
}
調用了unpark方法後,進行lock操作被阻塞的線程就恢復到運行狀態,就會再次執行acquireQueued中的無限for循環中的操作,再次嘗試獲取鎖。
AQS基本使用
同步器的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態。可重寫的方法有:
- tryAcquire 獨佔鎖獲取
- tryRelease 獨佔鎖釋放
- tryAcquireShared 共享鎖獲取
- tryReleaseShared 共享鎖釋放
- isHeldExclusively 快速判斷被線程獨佔
同步器的設計是基於模板方法模式, 使用者需要繼承同步器並重寫指定的方法,隨後將同步器組合在自定義同步組件的實現中,並調用同步器提供的模板方法,而這些模板方法將會調用使用者重寫的方法
對同步狀態進行更改,這時就需要使用同步器提供的3個方法來進行操作
- getState() 獲取同步狀態
- setState 設置同步狀態
- compareAndSetState 原子的設置同步狀態
來一個自定義AQS的例子:
public class AbstractQueuedSynchronizerDemo implements Lock {
private final Sync sync = new Sync(2);
// 靜態內部類,自定義同步器
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
if (count <= 0) {
throw new IllegalArgumentException("count must large than zero.");
}
setState(count);
}
public int tryAcquireShared(int reduceCount) {
for (;;) {
int current = getState();
int newCount = current - reduceCount;
if (newCount < 0 || compareAndSetState(current, newCount)) {
return newCount;
}
}
}
public boolean tryReleaseShared(int returnCount) {
for (;;) {
int current = getState();
int newCount = current + returnCount;
if (compareAndSetState(current, newCount)) {
return true;
}
}
}
final ConditionObject newCondition() {
return new ConditionObject();
}
}
public void lock() {
sync.acquireShared(1);
}
public void unlock() {
sync.releaseShared(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public boolean tryLock() {
return sync.tryAcquireShared(1) >= 0;
}
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(time));
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
public class AbstractQueuedSynchronizerMain {
public void test() {
final Lock lock = new AbstractQueuedSynchronizerDemo();
class Worker extends Thread {
public void run() {
while (true) {
lock.lock();
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 啓動10個子線程
for (int i = 0; i < 10; i++) {
Worker w = new Worker();
w.setDaemon(true);
w.start();
}
// 主線程每隔1秒換行
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println();
}
}
public static void main(String[] args) {
AbstractQueuedSynchronizerMain testMyLock = new AbstractQueuedSynchronizerMain();
testMyLock.test();
}
}
LockSupport
LockSupport介紹
LockSupport定義了一組的公共靜態方法,這些方法提供了最基本的線程阻塞和喚醒功能,而LockSupport也成爲構建同步組件的基礎工具,應用極其廣泛。LockSupport定義了一組以park開頭的方法用來阻塞當前線程,以及unpark(Thread thread)方法來喚醒一個被阻塞的線程,使用非常方便。
和Object的wait和notify/notifyAll對比
阻塞和喚醒線程的方式,按照以前我們可以使用Object的wait和notify/notifyAll方法實現,如下:
public class TestWait {
public static void main(String[] args)throws Exception {
final Object obj = new Object();
Thread A = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
try {
obj.wait();
}catch (Exception e){
e.printStackTrace();
}
System.out.println(sum);
}
});
A.start();
//睡眠一秒,保證線程A已經計算完成,阻塞在wait方法
Thread.sleep(1000);
obj.notify();
}
}
這一段代碼會報錯,因爲Object的wait和notify/notifyAll方法必須在同步代碼塊中才能使用!!!因此,我們改進一下:
public class TestWait {
public static void main(String[] args)throws Exception {
final Object obj = new Object();
Thread A = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
try {
synchronized (obj){
obj.wait();
}
}catch (Exception e){
e.printStackTrace();
}
System.out.println(sum);
}
});
A.start();
//睡眠一秒,保證線程A已經計算完成,阻塞在wait方法
Thread.sleep(1000);
synchronized (obj){
obj.notify();
}
}
}
我們再來看看使用LockSupport是如何簡單實現的:
public class TestWait {
public static void main(String[] args)throws Exception {
Thread A = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
LockSupport.park();
System.out.println(sum);
}
});
A.start();
LockSupport.unpark(A);
}
}
對比結果:
- LockSupport不需要在同步代碼塊裏,線程間不需要維護一個共享的同步對象,實現線程間的解耦。
- unpark函數可以先於park調用,所以不需要擔心線程間的執行的先後順序
原理分析
LockSupport的park方法內部調用了Unsafe的park方法,是一個本地native方法,只能通過openjdk的源碼查看其本地實現。本地實現中維護了一個int類型的變量_counter,所有的park和unpark都在圍繞這個變量進行的。
- 調用park時,判斷_counter是否大於零,如果大於零則將_counter設置爲零,然後退出,不進行等待阻塞(說明之前已經調用過unpark);如果不大於零(說明之前沒有調用過unpark),則進行等待阻塞
- 調用unpark時,現將_counter設置爲1,然後判斷先前的_counter的值是否小於1,如果不小於1,說明沒有線程被park,直接退出;如果小於1,則說明之前有線程park,此時需要喚醒
- 多次調用unpark方法和調用一次unpark方法效果一樣,因爲都是直接將_counter賦值爲1,而不是加1
- 線程A連續調用兩次LockSupport.unpark(B)方法喚醒線程B,然後線程B連續調用兩次LockSupport.park()方法, 線程B依舊會被阻塞。因爲兩次unpark調用效果跟一次調用一樣,只能讓線程B的第一次調用park方法不被阻塞,第二次調用依舊會阻塞
同步隊列
同步器依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理,當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等信息構造成爲一個節點(Node)並將其加入同步隊列。
同步器擁有首節點(head)和尾節點(tail),沒有成功獲取同步狀態的線程將會成爲節點加入該隊列的尾部
獨佔式同步狀態獲取與釋放
通過調用同步器的acquire(int arg)方法可以獲取同步狀態,其主要邏輯是:
- 首先調用自定義同步器實現的tryAcquire(int arg)方法,該方法保證線程安全的獲取同步狀態,如果同步狀態獲取失敗,則構造同步節點(獨佔式Node.EXCLUSIVE,同一時刻只能有一個線程成功獲取同步狀態)並通過addWaiter(Node node)方法將該節點加入到同步隊列的尾部。
- 最後調用acquireQueued(Node node,int arg)方法,使得該節點以“死循環”的方式獲取同步狀態。如果獲取不到則阻塞節點中的線程,而被阻塞線程的喚醒主要依靠前驅節點的出隊或阻塞線程被中斷來實現。
共享式同步狀態獲取與釋放
共享式獲取與獨佔式獲取最主要的區別在於同一時刻能否有多個線程同時獲取到同步狀態。
在acquireShared(int arg)方法中,同步器調用tryAcquireShared(int arg)方法嘗試獲取同步狀態,tryAcquireShared(int arg)方法返回值爲int類型,當返回值大於等於0時,表示能夠獲取到同步狀態。
因此,在共享式獲取的自旋過程中,成功獲取到同步狀態並退出自旋的條件就是tryAcquireShared(int arg)方法返回值大於等於0