Java多線程 同步隊列詳解(AQS)
文章目錄
1、AQS簡介
1.1 什麼是AQS
隊列同步器(AQS)是用來構建鎖或者其他同步組件的基礎框架,使用一個int型變量代表同步狀態,通過內置的隊列來完成線程的排隊工作。
根據其API,總結來說就是:
①子類通過繼承AQS並實現其抽象方法來管理同步狀態,對於同步狀態的更改通過提供的getState()、setState(int state)、compareAndSetState(int expect, int update)來進行操作,因爲使用CAS操作保證同步狀態的改變是原子的。
②子類被推薦定義爲自定義同步組件的靜態內部類,同步器本身並沒有實現任何的同步接口,僅僅是定義了若干狀態獲取和釋放的方法來提供自定義同步組件的使用。
③同步器既可以支持獨佔式的獲取同步狀態,也可以支持共享式的獲取同步狀態(ReentrantLock、ReentrantReadWriteLock、CountDownLatch等不同類型的同步組件)
ASQ定義了兩種資源共享的方式:
(1)獨佔,只有一個線程能執行,如ReentrantLock;
(2)共享,多個線程可以同時執行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
關於同步器的幾個重要方法 :
(1)sHeldExclusively():該線程是否正在獨佔資源。只有用到condition才需要去實現它。
(2)tryAcquire(int):獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false。
(3)tryRelease(int):獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
(4)tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。
(5)tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放後允許喚醒後續等待結點返回true,否則返回false。
1.2 什麼是CLH鎖隊列
CLH(Craig,Landin,and Hagersten)隊列是一個虛擬的雙向隊列,虛擬的雙向隊列即不存在隊列實例,僅存在節點之間的關聯關係。
CLH隊列是AQS很重要的組成部分,它是一個雙端隊列,遵循FIFO原則,主要作用是用來存放在鎖上阻塞的線程,當一個線程嘗試獲取鎖時,如果已經被佔用,那麼當前線程就會被構造成一個Node節點插入到同步隊列的尾部,隊列的頭節點是成功獲取鎖的節點,當頭節點線程釋放鎖時,會喚醒後面的節點並釋放當前頭節點的引用。
結構如下:
2、ASQ 提供的各種鎖實現流程
2.1 獨佔鎖
1、獲取鎖的過程
(1)調用入口方法acquire(arg)
(2)調用模版方法tryAcquire(arg)嘗試獲取鎖,若成功則返回,若失敗則走下一步
(3)將當前線程構造成一個Node節點,並利用CAS將其加入到同步隊列到尾部,然後該節點對應到線程進入自旋狀態
(4)自旋時,首先判斷其前驅節點是否爲頭節點&是否成功獲取同步狀態,兩個條件都成立,則將當前線程的節點設置爲頭節點,如果不是,則利用LockSupport.park(this)將當前線程掛起 ,等待被前驅節點喚醒
2、釋放鎖的過程
(1)調用入口方法release(arg)
(2)調用模版方法tryRelease(arg)釋放同步狀態
(3)獲取當前節點的下一個節點
(4)利用LockSupport.unpark(currentNode.next.thread)喚醒後繼節點(接獲取的第四步)
實例演示(ReentrantLock)可前往上一篇博客:
Java多線程 ReentrantLock與Condition
2.2 共享鎖
1、獲取鎖過程
(1)調用acquireShared(arg)入口方法
(2)進入tryAcquireShared(arg)模版方法獲取同步狀態,如果返返回值>=0,則說明同步狀態(state)有剩餘,獲取鎖成功直接返回
如果tryAcquireShared(arg)返回值<0,說明獲取同步狀態失敗,向隊列尾部添加一個共享類型的Node節點,隨即該節點進入自旋狀態
(3)自旋時,首先檢查前驅節點是否爲頭節點 && tryAcquireShared()是否>=0(即成功獲取同步狀態)
(4)如果是,則說明當前節點可執行,同時把當前節點設置爲頭節點,並且喚醒所有後繼節點
(5)如果否,則利用LockSupport.unpark(this)掛起當前線程,等待被前驅節點喚醒
2、釋放鎖過程
(1)調用releaseShared(arg)模版方法釋放同步狀態
(2)LockSupport.unpark(nextNode.thread)喚醒所有後繼節點。
3、實例演示(信號量Semphroe ):
Semaphore可以維護當前訪問自身的線程個數,並提供了同步機制,使用Semaphore可以控制同時訪問資源的線程個數,例如,實現一個文件允許的併發訪問數。Semaphore 只對可用許可的號碼進行計數,並採取相應的行動。
Semaphore實現的功能就像:銀行辦理業務,一共有5個窗口,但一共有10個客戶,一次性最多有5個客戶可以進行辦理,其他的人必須等候,當5個客戶中的任何一個離開後,在等待的客戶中有一個人可以進行業務辦理。
例如:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class SemaphoreTest {
public static void main(String[] args) {
//創建一個可根據需要創建新線程的線程池
ExecutorService service = Executors.newCachedThreadPool();
final Semaphore sp = new Semaphore(3);
//創建10個線程
for(int i=0;i<10;i++){
Runnable runnable = new Runnable(){
public void run(){
try {
sp.acquire(); //獲取燈,即許可權
} catch (InterruptedException e1) {
e1.printStackTrace();
}
System.out.println("線程" + Thread.currentThread().getName() +
"進入,當前已有" + (3-sp.availablePermits()) + "個併發");
try {
Thread.sleep((long)(Math.random()*10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程" + Thread.currentThread().getName() +
"即將離開");
sp.release(); // 釋放一個許可,將其返回給信號量
//下面代碼有時候執行不準確,因爲其沒有和上面的代碼合成原子單元
System.out.println("線程" + Thread.currentThread().getName() +
"已離開,當前已有" + (3-sp.availablePermits()) + "個併發");
}
};
service.execute(runnable);
}
}
}
結果:
線程pool-1-thread-3進入,當前已有3個併發
線程pool-1-thread-2進入,當前已有3個併發
線程pool-1-thread-1進入,當前已有3個併發
線程pool-1-thread-2即將離開
線程pool-1-thread-2已離開,當前已有2個併發
線程pool-1-thread-5進入,當前已有3個併發
線程pool-1-thread-1即將離開
線程pool-1-thread-1已離開,當前已有2個併發
線程pool-1-thread-4進入,當前已有3個併發
線程pool-1-thread-4即將離開
線程pool-1-thread-4已離開,當前已有2個併發
線程pool-1-thread-8進入,當前已有3個併發
線程pool-1-thread-3即將離開
線程pool-1-thread-7進入,當前已有3個併發
線程pool-1-thread-3已離開,當前已有3個併發
線程pool-1-thread-8即將離開
線程pool-1-thread-8已離開,當前已有2個併發
線程pool-1-thread-9進入,當前已有3個併發
線程pool-1-thread-7即將離開
線程pool-1-thread-7已離開,當前已有2個併發
線程pool-1-thread-6進入,當前已有3個併發
線程pool-1-thread-9即將離開
幫助理解:
(1)Semaphore sp = new Semaphore(3),創建了共享資源3個,相當於把state的值設置爲3;
(2)sp.acquire(); 獲取同步資源,若state > 0 , 獲取資源成功,可執行,並將state - 1; 否則獲取失敗,並加入線程同步隊列;
(3) sp.release(); 釋放同步資源,並將state + 1,把線程同步中的隊列都喚醒。
2.3 可重入鎖
重入鎖指的是當前線成功獲取鎖後,如果再次訪問該臨界區,則不會對自己產生互斥行爲。
Java中對ReentrantLock和synchronized都是可重入鎖,synchronized由jvm實現可重入即使,ReentrantLock都可重入性基於AQS實現。
1、ReentrantLock可重入鎖實現的核心代碼:
final boolean nonfairTryAcquire(int acquires) {
//獲取當前線程
final Thread current = Thread.currentThread();
//通過AQS獲取同步狀態
int c = getState();
//同步狀態爲0,說明臨界區處於無鎖狀態,
if (c == 0) {
//修改同步狀態,即加鎖
if (compareAndSetState(0, acquires)) {
//將當前線程設置爲鎖的owner
setExclusiveOwnerThread(current);
return true;
}
}
//如果臨界區處於鎖定狀態,且上次獲取鎖的線程爲當前線程
else if (current == getExclusiveOwnerThread()) {
//則遞增同步狀態
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
可見,可重鎖的實現重點是判斷當前正在執行的線程跟請求線程是否爲同一個線程。
實例演示(ReentrantLock)可前往上一篇博客:
Java多線程 ReentrantLock與Condition
2.4 公平鎖與非公平鎖
1、公平鎖
公平鎖是指當多個線程嘗試獲取鎖時,成功獲取鎖的順序與請求獲取鎖的順序相同;
對於公平鎖,線程獲取鎖的過程可以用如下示意圖表示
公平鎖搶鎖示意圖
2、非公平鎖
非公平鎖是指當鎖狀態爲可用時,不管在當前鎖上是否有其他線程在等待,新近線程都有機會搶佔鎖
示意圖
實例演示(ReentrantLock)可前往上一篇博客:
Java多線程 ReentrantLock與Condition
2.5 讀寫鎖
Java提供了一個基於AQS到讀寫鎖實現ReentrantReadWriteLock,該讀寫鎖到實現原理是:將同步變量state按照高16位和低16位進行拆分,高16位表示讀鎖,低16位表示寫鎖。
結構如下圖:
1、寫鎖是一個獨佔鎖,所以我們看一下ReentrantReadWriteLock中tryAcquire(arg)的實現:
獲取:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
(1)獲取同步狀態,並從中分離出低16爲的寫鎖狀態
(2)如果同步狀態不爲0,說明存在讀鎖或寫鎖
(3)如果存在讀鎖(c !=0 && w == 0),則不能獲取寫鎖(保證寫對讀的可見性)
(4)如果當前線程不是上次獲取寫鎖的線程,則不能獲取寫鎖(寫鎖爲獨佔鎖)
(5)如果以上判斷均通過,則在低16爲寫鎖同步狀態上利用CAS進行修改(增加寫鎖同步狀態,實現可重入)
(6)將當前線程設置爲寫鎖的獲取線程
釋放的過程與獨佔鎖基本相同。
2、讀鎖是一個共享鎖,獲取讀鎖的步驟如下:
(1)獲取當前同步狀態
(2)計算高16爲讀鎖狀態+1後的值
(3)如果大於能夠獲取到的讀鎖的最大值,則拋出異常
(4)如果存在寫鎖並且當前線程不是寫鎖的獲取者,則獲取讀鎖失敗
(5)如果上述判斷都通過,則利用CAS重新設置讀鎖的同步狀態
讀鎖的獲取步驟與寫鎖類似,即不斷的釋放寫鎖狀態,直到爲0時,表示沒有線程獲取讀鎖。
在JDK1.6以後,讀鎖的實現比上述過程更加複雜,有興趣的同學可以看一下最新的後去讀鎖的源碼。