1 AbstractQueuedSynchronizer之AQS
1.1 先從字節跳動及其它大廠面試題說起
- 從集合開始吧,介紹一下常用的集合類,哪些是有序的,哪些是無序的
- hashmap是如何尋址的,哈希碰撞后是如何存儲數據的,1.8后什么時候變成紅黑樹、說下紅黑樹原理有什么好處?
- reentrantlock實現原理,簡單說下AQS
- synchronized實現原理,monitor對象是什么時候生成的?知道monitor和monitorenter和monitorexit這2個是怎么保證同步的嗎,或者說底層是如何執行的
- 偏向鎖和輕量級鎖有什么區別?
1.2 前置知識
- 公平鎖和非公平鎖
- 可重入鎖
- LockSupport
- 自旋鎖
- 數據結構之鏈表
- 設計模式之模板設計模式
1.3 抽象的隊列同步器
AbstractOwnableSynchronizer
AbstractQueuedLongSynchronizer
AbstractQueuedSynchronizer
通常地: AbstractQueuedSynchronizer簡稱為AQS
是用來構建鎖或者其它同步器組件的重量級基礎框架及整個JUC體系的基石, 通過內置的FIFO隊列來完成資源獲取線程的排隊工作,并通過一個int類變量 表示持有鎖的狀態
1.4 AQS為什么是JUC內容中最重要的基石
和AQS有關的
ReentrantLock
CountDownLatch
ReentrantReadWriteLock
Semaphore
進一步理解鎖和同步器的關系
- 鎖,面向鎖的使用者
- 定義了程序員和鎖交互的使用層API,隱藏了實現細節,你調用即可。
- 同步器,面向鎖的實現者
- 比如Java并發大神Douglee,提出統一規 范并簡化了鎖的實現,屏蔽了同步狀態管理、阻塞線程排隊和通知、喚醒機制等。
能干嘛
-
加鎖會導致阻塞
- 有阻塞就需要排隊,實現排隊必然需要有某種形式的隊列來進行管理
搶到資源的線程直接使用辦理業務,搶占不到資源的線程的必然涉及一種排隊等候機制,搶占資源失敗的線程繼續去等待(類似辦理窗口都滿了,暫時沒有受理窗口的顧客只能去候客區排隊等候),仍然保留獲取鎖的可能且獲取鎖流程仍在繼續(候客區的顧客也在等著叫號,輪到了再去受理窗口辦理業務)。
既然說到了排隊等候機制,那么就一定會有某種隊列形成,這樣的隊列是什么數據結構呢?
如果共享資源被占用,就需要一定的阻塞等待喚醒機制來保證鎖分配。這個機制主要用的是CLH隊列的變體實現的,將暫時獲取不到鎖的線程加入到隊列中,這個隊列就是AQS的抽象表現。它將請求共享資源的線程封裝成隊列的結點(Node) ,通過CAS、自旋以及LockSuport.park()的方式,維護state變量的狀態,使并發達到同步的效果。
1.5 AQS初步
1.5.1 AQS初識
有阻塞就需要排隊,實現排隊必然需要隊列
AQS使用一個volatile的int類型的成員變量來表示同步狀態,通過內置的 FIFO隊列來完成資源獲取的排隊工作將每條要去搶占資源的線程封裝成 一個Node節點來實現鎖的分配,通過CAS完成對State值的修改。
1.5.2 AQS內部體系架構
AQS自身
-
AQS的int變量
- AQS的同步狀態State成員變量
/** * The synchronization state. */ private volatile int; //銀行辦理業務的受理窗口狀態 //零就是沒人,自由狀態可以辦理 //大于等于1,有人占用窗口,等著
-
AQS的CLH隊列
- CLH隊列(三個大牛的名字組成),為一個雙向隊列
state變量+CLH雙端Node隊列
內部類Node(Node類在AQS類內部)
- Node的int變量
- Node的等待狀態waitState成員變量 volatile int waitStatus
- 等候區其它顧客(其它線程)的等待狀態
- 隊列中每個排隊的個體就是一個Node.
1.5.3 AQS同步隊列的基本結構
是用LockSupport.pork()來進行排隊的
1.6 AQS詳解(ReentrantLock開始解讀AQS)
1.6.1 Code
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class AQSDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
//帶入一個銀行辦理業務的案例來模擬我們的AQS如何進行線程的管理和通知喚醒機制
//3個線程模擬3個來銀行網點,受理窗口辦理業務的顧客
//A顧客就是第一個顧客,此時受理窗口沒有任何人,A可以直接去辦理
new Thread(() -> {
lock.lock();
try {
System.out.println("-----A thread come in");
try {
TimeUnit.MINUTES.sleep(20);
} catch (Exception e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}, "A").start();
//第二個顧客,第二個線程---》由于受理業務的窗口只有一個(只能一個線程持有鎖),此時B只能等待,
//進入候客區
new Thread(() -> {
lock.lock();
try {
System.out.println("-----B thread come in");
} finally {
lock.unlock();
}
}, "B").start();
//第三個顧客,第三個線程---》由于受理業務的窗口只有一個(只能一個線程持有鎖),此時C只能等待,
//進入候客區
new Thread(() -> {
lock.lock();
try {
System.out.println("-----C thread come in");
} finally {
lock.unlock();
}
}, "C").start();
}
}
Lock接口的實現類,基本都是通過【聚合】了一個【隊列同步器】的子類完成線程訪問控制的
1.6.2 ReentrantLock原理
1.6.3 lock方法的公平和非公平
通過ReentrantLock的源碼來講解公平鎖和非公平鎖
默認創建的是非公平鎖
可以明顯看出公平鎖與非公平鎖的lock()方法唯一的區別就在于公平鎖在獲取同步狀態時多了一個限制條件:
hasQueuedPredecessors()
hasQueuedPredecessors是公平鎖加鎖時判斷等待隊列中是否存在有效節點的方法
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
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());
}
對比公平鎖和非公平鎖的tryAcqure()方法的實現代碼, 其實差別就在于非公平鎖獲取鎖時比公平鎖中少了一個判斷!hasQueuedPredecessors()
hasQueuedPredecessors()中判斷了是否需要排隊,導致公平鎖和非公平鎖的差異如下:
公平鎖:公平鎖講究先來先到,線程在獲取鎖時,如果這個鎖的等待隊列中已經有線程在等待,那么當前線程就會進入等待隊列中;
非公平鎖:不管是否有等待隊列,如果可以獲取鎖,則立刻占有鎖對象。也就是說隊列的第一個排隊線程在unpark(), 之后還是需要競爭鎖(存在線程競爭的情況下)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
1.6.4 AQS源碼深度分析
lock()
acquire()
tryAcquire(arg) 非公平鎖
繼續推進條件,走下一步方法addWaiter,否則結束。
addWaiter(Node.EXCLUSIVE)
雙向鏈表中,第一個節點為虛節點(也叫哨兵節點),其實并不存儲任何信息,只是占位。 真正的第一個有數據的節點,是從第二個節點開始的。
假如3號ThreadC線程進來,prev-compareAndSetTail-next
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
假如再搶搶失敗就會進入shouldParkAterFailedAcquire和parkAndCheckInterrupt方法中
shouldParkAfterFailedAcquire
如果前驅節點的waitstatus是SIGNAL狀態(-1),即shouldParkAfterFailedAcquire方法會返回true 程序會繼續向下執行parkAndCheckInterrupt方法,用于將當前線程掛起
parkAndCheckInterrupt
方法unlock()
sync.release(1);
tryRelease(arg)
unparkSuccessor
1.6.5 AQS面試考點
你應該看過源碼了,那么AQS里面有個變量叫State,它的值有幾種?
3個狀態:沒占用是0,占用了是1,大于1是可重入鎖
如果AB兩個線程進來了以后,請問這個總共有多少個Node節點?
答案是3個