JUC包下的AQS — 隊列同步器
AQS簡介
AQS,即AbstractQueuedSynchronizer,在java.util.concurrent.locks包下面。AQS是用來構建鎖和同步器的框架,基於AQS可以簡單高效的開發出適合自己的同步器。
ReentrantLock,Semaphore,其他的諸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基於AQS的,AQS可以認爲是JUC的核心。
可重入鎖非可重入鎖
在講解原理之前,我們先弄清楚兩個概念,可重入鎖非可重入鎖。可重入鎖指的是當一個線程獲取鎖之後,可以再次獲取該鎖,從而避免死鎖。
ReentrantLock和synchronized都是可重入鎖,AQS中共享變量state的初始值爲0。對於可重入鎖,當一個線程嘗試獲取鎖時,會判斷state是否等於0,等於0則可以獲取鎖,並將state置爲1,若state值不是0,會判斷獲取鎖的線程是否是佔有鎖的線程,如果是則執行state++。釋放鎖時,同樣的也會執行state–,直到state等於0,線程纔會真正釋放鎖。
對於非可重入鎖,當一個線程嘗試獲取鎖時,判斷state值是不是0,是0則可以獲取鎖,不是0則進入阻塞狀態,如果是同一個線程就會產生死鎖。
樂觀鎖和悲觀鎖
悲觀鎖
總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完後再把資源轉讓給其它線程)。傳統的關係型數據庫裏邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。Java中synchronized和ReentrantLock等獨佔鎖就是悲觀鎖思想的實現。
樂觀鎖
總是假設最好的情況,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號機制和CAS算法實現。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫提供的類似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。
這篇博客對於樂觀鎖和悲觀鎖寫的非常詳細。
面試必備之樂觀鎖與悲觀鎖
AQS原理
1.使用Node實現FIFO隊列,可以用於構建鎖或者其他同步裝置的基礎框架。
2.使用一個int類型表示狀態。
private volatile int state;//共享變量,使用volatile修飾保證線程可見性
3.子類通過繼承並通過實現它的方法管理其狀態{acquire和release}的方法操縱狀態。
4.可以同時實現排它鎖和共享鎖模式(獨佔、共享),指的是實現它的子類可以選擇獨佔或者共享,不是說可以選擇獨佔和共享。
Exclusive(獨佔): 只有一個線程能執行,如ReentrantLock。又可分爲公平鎖和非公平鎖。 公平鎖:按照線程在隊列中的排隊順序,先到者先拿到鎖。
非公平鎖:當線程要獲取鎖時,無視隊列順序直接去搶鎖,誰搶到就是誰的。
Share(共享): 多個線程可同時執行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、CyclicBarrier、ReadWriteLock。
AQS的核心思想是: 當共享資源空閒,則將當前請求資源的線程設置爲有效的工作線程,並且將共享資源設置爲鎖定狀態。如果被請求的共享資源被佔用,那麼就需要一套線程阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH隊列鎖實現的,即將暫時獲取不到鎖的線程加入到隊列中。
並且只有head節點的直接後繼節點可以請求獲取鎖,失敗則將自己阻塞直到獲取成功。
AQS同步組件
CountDownLatch
在程序執行過程中,可能需要滿足某些條件纔可以繼續執行後續的操作,所以JDK1.5之後引入了該類。
//調用該方法的線程會被暫時掛起,直到count值減到0,纔會被喚醒繼續執行。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//和await()方法用法相同,可以設置時間,當一段時間後沒有達到條件也會被繼續執行
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
//令count值減一
public void countDown() {
sync.releaseShared(1);
}
CountDownLatch可以使一個線程等待其他線程各自執行完畢後再執行,但是不支持count值進行重置,如果業務上有要求的話,可以考慮使用CyclicBarrier。
Semaphore(信號量)
可以控制某個資源可以被同時訪問的線程個數,下面這是兩個比較重要的方法。
//獲得一個許可
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//方法可以傳入permits參數,代表依次獲取多個,同樣可以依次釋放多個
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
//釋放一個許可
public void release() {
sync.releaseShared(1);
}
例子:
private static int threadCount = 20;
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
exec.execute(() -> {
try {
//獲取許可
semaphore.acquire();
test(threadNum);
//釋放許可
semaphore.release();
} catch (InterruptedException e) {
log.info("exception",e);
}
});
}
log.info("finish");
exec.shutdown();
}
private static void test(int threadNum) throws InterruptedException {
log.info("{}",threadNum);
Thread.sleep(1000);
}
從這個例子可以看出,執行結果是3個3個出現的,對應了new Semaphore(3)將信號量設置爲3。
Semaphore還有一個比較重要的方法:tryAcquire()
//嘗試獲取信號量,在特定時間獲取不到則放棄執行
public boolean tryAcquire(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
CyclicBarrier
CyclicBarrier和CountDownLatch用法很相似,只不過CountDownLatch執行的是減一操作,CyclicBarrier執行的是加一操作。可以實現一組線程相互等待,直到到達某個條件纔會繼續執行,當一個線程調用await()方法時,就會進入阻塞狀態,直到達到臨界值纔會喚醒所有等待的線程繼續執行。
比如可以對多個目標進行相加的操作時,需要等待每個線程都操作完畢時再進行統一相加,就可以使用CyclicBarrier來實現。相對的,CyclicBarrier的臨界值可以循環使用。
//可以設置等待時間,超過時間就算沒有到達指定條件,依然會執行
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
例子:
private static CyclicBarrier barrier = new CyclicBarrier(5);
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int threadNum = i;
Thread.sleep(1000);
exec.execute(() -> {
try {
race(threadNum);
} catch (Exception e) {
log.info("Exception", e);
}
});
}
exec.shutdown();
}
private static void race(int threadNum) throws Exception {
Thread.sleep(1000);
log.info("{} is ready", threadNum);
try {
barrier.await(2000, TimeUnit.MILLISECONDS);
} catch (BrokenBarrierException e) {
log.info("BarrierException", e);
}
log.info("{} continue", threadNum);
}
需要注意的是: 設置時間可能會拋出BrokenBarrierException異常,如果希望下面的代碼繼續執行,就需要將異常捕獲而不讓其拋出。
BrokenBarrierException這個異常的意思就是,當某個等待的線程被中斷,其它等待的線程會拋出這個異常。
//這樣做可以在達到條件時,先執行這裏的代碼
private static CyclicBarrier barrier = new CyclicBarrier(5, () -> {
log.info("callback is running");
});
ReentrantLock(可重入鎖)
ReentrantLock和synchronized的區別:
① 兩者都是可重入鎖
② synchronized 依賴於 JVM 而 ReenTrantLock 依賴於 API
③ReenTrantLock 比 synchronized 增加了一些高級功能
1.ReenTrantLock提供了一種能夠中斷等待鎖的線程的機制,synchronized 則不能。
2.ReenTrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。
3.ReentrantLock類線程對象可以註冊在指定的Condition中,從而可以有選擇性的進行線程通知,在調度線程上更加靈活。 在使用notify/notifyAll()方法進行通知時,被通知的線程是由 JVM 選擇的,用ReentrantLock類結合Condition實例可以實現選擇性通知
在synchronized可以實現的情況下,還是比較推薦使用synchronized。
我們來看一下部分源碼
//無參構造方法,默認給了一個不公平鎖
public ReentrantLock() {
sync = new NonfairSync();
}
//有參構造方法,可以自己設着公平或非公平
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
//嘗試獲取鎖,如果沒有被其它線程鎖定,則獲取鎖
//也可以傳入時間,代表一段時間後,鎖沒有被其它線程鎖定,則獲取鎖
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
例子:
//聲明一個鎖
private final static Lock lock = new ReentrantLock();
//切記解鎖一定要放在finally中,防止出現死鎖的情況
private static void add() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
ReentrantReadWriteLock(讀寫鎖)
在沒有任何讀寫鎖的時候,纔可以獲得寫鎖,這實際上是用的悲觀鎖的設計思想。所以在讀操作頻繁,寫操作不頻繁的情況下,有可能會造成飢餓現象。比如,一個線程希望進行寫操作,但是由於寫操作很頻繁,寫進程就會一直等待。
用法:
private final Map<String, Data> map = new TreeMap<>();
private final static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public Data get(String key){
readLock.lock();
try{
return map.get(key);
}finally {
readLock.unlock();
}
}
public Set<String> getAllKeys(){
readLock.lock();
try{
return map.keySet();
}finally {
readLock.unlock();
}
}
public Data put(String key,Data value){
writeLock.lock();
try{
return map.put(key,value);
}finally {
readLock.unlock();
}
}
class Data{
}
Condition
前面在介紹AQS時,我們知道AQS實際上是通過內置的FIFO隊列來完成獲取資源線程的排隊工作。
實際上在這個隊列下面還存在着一個Condition隊列,暫時可以將其理解爲一個假死隊列,調用方法進入該隊列的線程就像是死掉了一樣,但是它是在等待一個時機,條件達到的時候可以調用方法將其喚醒繼續執行。
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
Condition condition = reentrantLock.newCondition();
new Thread(() ->{
try{
reentrantLock.lock();
log.info("wait signal");//1
condition.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
log.info("get signal");//4
reentrantLock.unlock();
}).start();
new Thread(() -> {
reentrantLock.lock();
log.info("get lock");//2
try{
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
condition.signalAll();
log.info("send signal");//3
reentrantLock.unlock();
}).start();
}
代碼中的的註釋代表的是執行的順序,當線程執行condition. wait()之後,該線程就會進入Condition隊列,並且相當於執行了unlock()進行了解鎖。
當另一個線程嘗試獲取該線程時可以獲取,當執行condition.signalAll()之後,就會喚醒Condition隊列中所有的線程,當然也可以一個一個的喚醒,之後繼續執行。
實際上,用到Condition的場景很少,這裏當做瞭解。