J.U.C之AQS:AQS詳解和使用
AQS是什么
AQS是AbstractQueuedSynchronizer的縮寫,翻譯過來就是"同步器",AbstractQueuedSynchronizer是一個抽象類,它實現(xiàn)了Java函數(shù)中鎖同步(synchronized)液样,鎖等待(wait,notify)功能寓搬。
Java并包里大部分并發(fā)工具類都將其作為核心基礎構件寥枝,比如可重入鎖ReentrantLock, 信號量Semaphore基于各自的特點來使用AQS提供的基礎能力方法實現(xiàn)多線程交互碍拆。
AQS核心功能
鎖同步(synchronized)
鎖等待(wait,notify)
AQS 中概念
同步狀態(tài)
AQS實現(xiàn)了鎖蓉驹,必然需要一個競爭對象列另。AQS存在從一個int類型的成員變量state,我們把它稱為同步狀態(tài),基于開閉原則往弓,內部提供了很多模板方法【參考AQS核心方法】給子類去定制如何獲取釋放同步狀態(tài)疏唾。
AQS按照獲取釋放同步狀態(tài)的方式分為"獨占式同步","共享式同步"。
獨占式同步
從概念上來說獨占式對應只存在一個資源函似,且只能被一個線程或者說競爭者占用.
共享式同步
從概念上來共享式對應存在多個資源的是有多個線程或者競爭者能夠獲取占用槐脏。他們對應的場景不同因而流程上會有差異
同步隊列
AQS 實現(xiàn)了鎖那么總需要一個隊列將無法獲取鎖的線程保存起來,方便在鎖釋放時通知隊列中線程去重新競爭鎖缴淋。
同步隊列又被稱為CLH同步隊列准给,CLH隊列是通過鏈式方式實現(xiàn)FIFO雙向隊列。當線程獲取同步狀態(tài)失敗時重抖,AQS則會將當前線程構造成一個節(jié)點(Node)并將其加入到CLH同步隊列露氮,同時會阻塞當前線程,當同步狀態(tài)被釋放時钟沛,會把首節(jié)點后第一個節(jié)點的線程從阻塞狀態(tài)下喚醒畔规,喚醒的線程會嘗試競爭同步狀態(tài),如果獲取同步狀態(tài)成功恨统,則從同步隊列中出隊,如果獲取同步狀態(tài)失敗則繼續(xù)阻塞叁扫。
栗子
public class LockDemo {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(() -> {
lock.lock();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
thread.start();
}
}
}
實例代碼中開啟了5個線程三妈,先獲取鎖之后再睡眠10S中。通過debug莫绣,當Thread-4(在本例中最后一個線程)獲取鎖失敗后進入同步時畴蒲,AQS時現(xiàn)在的同步隊列如圖所示
條件隊列
Java 傳統(tǒng)的監(jiān)視器有如下函數(shù) wait、notify对室、notifyAll模燥。
它們可以實現(xiàn)當一個線程獲取鎖時,它可以主動放棄(wait)鎖進入阻塞狀態(tài)掩宜,同時被添加進入一個條件隊列中蔫骂。只有其他線程通知喚醒(notify/notifyAll)時才從條件隊列中出隊,并嘗試獲取鎖牺汤,并在獲取鎖成功后繼續(xù)執(zhí)行之前的未完成代碼邏輯辽旋。
AQS內部存在一個內部類實現(xiàn)了Condition接口, 在AQS內部維護著一條鏈表實現(xiàn)單向條件隊列。使用AQS獲取內部實現(xiàn)Condition接口對象檐迟,調用await()补胚,signal(),signalAll()函數(shù)實現(xiàn)Java中wait、notify锅减、notifyAll同樣功能糖儡。
當獲取同步狀態(tài)的線程調用condition.await(),當前線程會阻塞,并進入一個等待隊列,釋放同步狀態(tài).
當其他線程調用了condition.signal()方法,會從等待隊列firstWaiter開始選擇第一個等待狀態(tài)不是取消的節(jié)點.添加到同步隊列尾部.
當其他線程調用了condition.signalAll()方法,會從等待隊列firstWaiter開始選擇所有等待狀態(tài)不是取消的節(jié)點.添加到同步隊列尾部.
這里取消節(jié)點表示當前節(jié)點的線程不在參與排隊獲取鎖。
栗子
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
lock.lock();
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
});
thread.start();
}
}
新建了10個線程怔匣,每個線程先獲取鎖,然后調用condition.await方法釋放鎖將當前線程加入到等待隊列中桦沉,通過debug控制當走到第10個線程的時候查看firstWaiter即等待隊列中的頭結點每瞒,debug模式下情景圖如下:
AQS 實現(xiàn)原理
AQS核心是一個同步狀態(tài),兩個隊列纯露。它們實現(xiàn)了Java函數(shù)中鎖同步(synchronized)剿骨,鎖等待(wait,notify),并在其基礎上實現(xiàn)了獨占式同步埠褪,共享式同步2中方式鎖的實現(xiàn)浓利。
無論獨占式還時共享式獲取同步狀態(tài)成功則直接返回,
當線程獲取同步狀態(tài)失敗時钞速,AQS則會將當前線程構造成一個節(jié)點(Node)并將其加入到CLH同步隊列贷掖,同時會阻塞當前線程,
當同步狀態(tài)被釋放時渴语,會把首節(jié)點后第一個節(jié)點的線程從阻塞狀態(tài)下喚醒苹威,喚醒的線程會嘗試競爭同步狀態(tài),如果獲取同步狀態(tài)成功驾凶,則從同步隊列中出隊,如果獲取同步狀態(tài)失敗則繼續(xù)阻塞牙甫。
當獲取同步狀態(tài)的線程調用condition.await(),當前線程會阻塞,并進入一個等待隊列,釋放同步狀態(tài).掷酗。
當其他線程調用了condition.signal()方法,會從等待隊列firstWaiter開始選擇第一個等待狀態(tài)不是取消的節(jié)點.添加到同步隊列尾部.
當其他線程調用了condition.signalAll()方法,會從等待隊列firstWaiter開始選擇所有等待狀態(tài)不是取消的節(jié)點.添加到同步隊列尾部.
這里取消節(jié)點表示當前節(jié)點的線程不在參與排隊獲取鎖。
AQS核心方法
獨占式同步
acquire
獲取同步狀態(tài)窟哺。如果當前線程獲取同步狀態(tài)成功則直接返回泻轰,如果獲取失敗插入同步隊列尾部,同時線程阻塞且轨。當調用release釋放同步狀態(tài)時糕殉,會從同步隊列head頭部后第一個節(jié)點中線程從阻塞中釋放并在自旋中重新競爭同步狀態(tài),如果獲取成功則從同步隊列出隊殖告,并返回阿蝶,如果獲取失敗則繼續(xù)阻塞,等待下次喚醒黄绩。
acquireInterruptibly
獨占式獲取同步狀態(tài),與acquire方法相同羡洁。但在如果當前線程被中斷,則該方法會拋出InterruptedException異常并返回爽丹;
tryAcquireNanos
獨占式獲取同步狀態(tài),與acquireInterruptibly方法相同筑煮。但在acquireInterruptibly基礎上增加了超時等待功能,在超時時間內沒有獲得同步狀態(tài)返回false
release
釋放獨占式同步狀態(tài)粤蝎,喚醒同步隊列中首節(jié)點之后的第一個等待節(jié)點的線程的阻塞真仲。
共享式同步
acquireShared
獲取同步狀態(tài),如果當前線程獲取同步狀態(tài)成功則直接返回。如果獲取失敗插入同步隊列尾部初澎,同時線程阻塞秸应。當調用releaseShared釋放同步狀態(tài)時,會找到從head頭部節(jié)點后置節(jié)點中的線程碑宴,并將該線程從阻塞中釋放软啼。被釋放的線程會在自旋中重新競爭同步狀態(tài)。如果獲取成功則出隊延柠,同時會釋放后置節(jié)點中的線程從阻塞中喚醒競爭同步狀態(tài)祸挪。
acquireSharedInterruptibly
在acquireShared方法基礎上增加了能響應中斷的功能;
tryAcquireSharedNanos
在acquireSharedInterruptibly基礎上增加了超時等待的功能贞间;
releaseShared
釋放共享式同步狀態(tài)贿条,釋放共享式同步狀態(tài)會喚醒同步隊列中首節(jié)點之后的第一個等待節(jié)點的線程的阻塞。
同步狀態(tài)
getState()
返回同步狀態(tài)的當前值
setState(int newState)
設置當前同步狀態(tài)
compareAndSetState(int expect, int update)
使用CAS設置當前狀態(tài)增热,該方法能夠保證狀態(tài)設置的原子性
同步隊列
hasQueuedThreads()
查詢是否有任何線程正在等待獲取整以。【在同步隊列中是否存在等待線程】
int getQueueLength()
返回等待獲取的線程數(shù)的估計值.在同步隊列中是否存在等待線程數(shù)量】
getQueuedThreads()
返回包含可能等待獲取的線程的集合钓葫。因為實際的線程集可能在構造此結果時動態(tài)地改變悄蕾,所以返回的集合僅是盡力而為的估計值【返回同步隊列中線程集合】
AQS模板方法
我們可以編寫自己類繼承AQS選擇重寫獨占式或共享式模板方法,從而定義如何獲取同步狀態(tài)和釋放同步狀態(tài)的邏輯。
獨占式
tryAcquire
嘗試獨占式獲取同步狀態(tài),返回值為true則獲得同步狀態(tài)成功,否則獲取失敗帆调。
調用場景:
1 在acquire開始判斷獲取同步狀態(tài)時調用
2 在acquire自旋循環(huán)中每次都會獲取同步狀態(tài)時調用(該線程從阻塞中釋放,會在自旋中重新競爭同步狀態(tài))
tryRelease
嘗試獨占式釋放同步狀態(tài)奠骄,返回值為true則表示獲取成功,否則獲取失敗番刊。
調用場景:
- 1 該方法在釋放獨占式同步狀態(tài)【release方法】時
共享式
tryAcquireShared
嘗試共享式獲取同步狀態(tài),當返回值為大于等于0的時獲得同步狀態(tài)成功含鳞,否則獲取失敗。
調用場景:
1 在acquireShared開始判斷獲取同步狀態(tài)時調用芹务。
2 在acquireShared自旋循環(huán)中每次都會獲取同步狀態(tài)時調用
(該線程從阻塞中釋放,會在自旋中重新競爭同步狀態(tài))
tryReleaseShared
嘗試共享式釋放同步狀態(tài)蝉绷,返回值為true則表示獲取成功,否則獲取失敗枣抱。
調用場景:
- 該方法在釋放共享式同步狀態(tài)【releaseShared方法】時會調用熔吗。
獨占式 VS 共享式
從概念上來說獨占式對應只存在一個資源,且只能被一個線程或者說競爭者占用佳晶,而共享式對應存在多個資源的是有多個線程或者競爭者能夠獲取占用桅狠。他們對應的場景不同因而流程上會有差異。
流程區(qū)別
在流程上來看只有加粗的部分是共享式所獨有的:
嘗試獲取同步失敗 --> 進入等待隊列排隊 --> 阻塞當前線程 --> 當?shù)却犃信诺阶约罕粏拘?--> 嘗試獲取鎖(可能被其他線程插隊而導致獲取鎖失敗轿秧,失敗在次阻塞中跌,等待下次排到自己)--> 嘗試獲取鎖成功,通知等待隊列前面共享節(jié)點線程從阻塞中喚醒 --> 執(zhí)行自己的業(yè)務邏輯 --> 嘗試釋放鎖--> 成功通知等待隊列前面共享節(jié)點線程從阻塞中喚醒菇篡。
獨占式
對于獨占式漩符,當節(jié)點中線程從阻塞中釋放,獲取同步狀態(tài)成功后驱还,就開始執(zhí)行嗜暴,直到完成,釋放同步狀態(tài)才會通知等待隊列節(jié)點線程從阻塞中喚醒铝侵,由于獨占式通常只有一個資源灼伤。因而就沒有必要在獲取鎖成功后通知同步隊列線程去嘗試獲取同步狀態(tài),因為自己還沒做完呢咪鲜,也就符合同時只能一人執(zhí)行特性。只有自己執(zhí)行完后才通知同步隊列線程獲取同步狀態(tài)撞鹉。
共享式
對于共享式疟丙,當節(jié)點中線程從阻塞中釋放,獲取同步成功鸟雏,在開始執(zhí)行任務前享郊,由于存在多個資源。因而更加積極孝鹊,他通知后置節(jié)點線程從阻塞中喚醒炊琉,如果后置節(jié)點同樣獲取同步狀態(tài)成功,則相當于同時有多個人在執(zhí)行,也可以說釋放鎖可以多個線程進入苔咪。且這種方式具有傳播性锰悼。直到某個節(jié)點獲取同步狀態(tài)失敗才停止這種傳播。
相同點
無論是獨占和共享都提供了模板方法去定制能否獲取同步和能否釋放同步团赏。
小結
能否獲取同步決定了同時又多少人同時執(zhí)行箕般。和資源相關和獨占式還是共享式無關。對于當個資源我們通常使用獨占式舔清,對于多個資源我們通常使用共享式丝里。又時候你甚至將獲取同步寫成默認返回true,表示無鎖。就好像是一個閘門絕對能放如多少水進入水庫体谒。是不是獨占也能實現(xiàn)共享鎖呢杯聚?答案是可以的。只不過他并沒有共享式那么積極抒痒!